# API compliance: Practical approaches for modern development teams Source: https://speakeasy.com/api-design/api-compliance import { Callout } from "@/mdx/components"; There's nothing quite like hitting your stride with a large API project. Your rate limiting, authentication, and state management have been debated, designed, built, and tested to perfection. It's time to start building new features for your growing user base. Bliss. But right as you're ready to ship, your in-house counsel emerges from a Patagonia vest catalog asking for "just a short review". A "quick coffee" is about to turn into the longest month of your life. At best, you're looking at weeks of meetings and documentation. At worst, your next eight sprints will be spent refactoring for compliance. It doesn't have to be this way. By integrating compliance requirements into your development process from the start, you can avoid these last-minute fire drills. This guide explores practical, industry-tested approaches to API compliance that are actually used in the real world. Let's start with what each role player wants and think about how we can find common ground: - **Developers** need clear, implementable specifications that don't sacrifice velocity. - **Legal teams** require documented controls and evidence for audits and reporting. - **Auditors** expect verifiable compliance and a clear audit trail. ## Why is compliance so important? Compliance allows you to avoid paying hefty fines and losing customer trust. Take, for example, [the €290,000,000 GDPR fine Uber had to pay in July 2024](https://web.archive.org/web/20250712030312/https://www.edpb.europa.eu/news/news/2024/dutch-sa-imposes-fine-290-million-euro-uber-because-transfers-drivers-data-us_en) for improperly transferring European drivers' personal data to servers in the United States. Uber's API and data infrastructure failed to implement adequate safeguards when transmitting sensitive information, including location data, payment details, identification documents, and in some cases, even criminal and medical records. This serves as a costly reminder that privacy compliance needs to be designed into APIs from the start, particularly for international data transfers. The cost of retrofitting compliance can far exceed the cost of building it in from the start. ## Common compliance frameworks In an ideal world, we'd have a single, universally accepted compliance framework. In reality, we have a slew of industry-specific standards and regulations. Here's a comparison of key requirements across major frameworks: | Framework | Access Control | Audit Logging | Data Minimization | Encryption | Breach Notification | |-----------|----------------|---------------|-------------------|------------|---------------------| | SOC 2 | ✓ | ✓ | – | ✓ | ✓ | | GDPR | ✓ | ✓ | ✓ | ✓ | ✓ | | PCI DSS | ✓ | ✓ | – | ✓ | ✓ | | HIPAA | ✓ | ✓ | ✓ | ✓ | ✓ | | CCPA | ✓ | ✓ | ✓ | – | ✓ | Let's explore each framework's specific requirements. ### SOC 2: System and Organization Controls The SOC 2 is the de facto standard for SaaS companies, especially those selling to enterprises. It's less about specific technical requirements and more about proving you have the proper controls in place. For APIs, this typically means: - Access control and authentication - Audit logs of all data access attempts and changes - Monitoring and alerting systems - Documented change management ### GDPR: General Data Protection Regulation If you handle data from EU residents (notice we didn't say EU citizens), the GDPR applies to you. It's a comprehensive piece of legislation that gives individuals strong rights over their personal data. Your API needs to support: - Data exports in machine-readable formats - Complete data deletion ("right to be forgotten") - Explicit consent tracking - Data minimization (only collect what you need) ### PCI DSS: Payment Card Industry Data Security Standard If your API handles credit card data, you need to comply with the PCI DSS. Unlike the SOC 2 and the GDPR, it has very specific technical requirements about how you handle, store, and transmit payment data. The PCI DSS is more prescriptive but focuses on storage and transmission: - Never store sensitive data (such as CVVs, PINs, or magnetic stripes). - Encrypt transmissions of cardholder data. - Restrict access by IP and authentication. - Log all access attempts and changes. ### HIPAA: Health Insurance Portability and Accountability Act The HIPAA governs the handling of protected health information (PHI) in the United States. Its Security Rule has specific requirements for API implementations: - Strong access controls with role-based authorization - End-to-end encryption for all PHI transmission - Detailed audit trails of all PHI access and modifications - Automatic session termination after a period of inactivity - Business Associate Agreements (BAAs) for API integrations - Secure API key management and rotation ### CCPA/CPRA: California Consumer Privacy Act/California Privacy Rights Act California's privacy laws give consumers significant rights over their personal information and impose strict requirements on APIs handling Californian residents' data, including: - The right to know what personal information is collected and shared - The right to delete personal information - The right to opt out of data sharing and selling - Data portability requirements - Specific consent requirements for minors - The requirement to honor Global Privacy Control (GPC) signals ## High-impact areas for compliance By focusing on high-impact areas, like data encryption, access control, audit logging, and data minimization, you can build a solid foundation for compliance. ### Data encryption Ensure that all sensitive data is encrypted at rest and in transit. Use industry-standard encryption algorithms and key management practices. ### Access control Zero trust principles should guide your access control strategy. Implement role-based access control (RBAC) and least privilege access to limit exposure. ### Audit logging Log all access attempts, changes, and deletions of sensitive data. Ensure logs are tamper-proof and stored securely. ### Data minimization Only collect data that is necessary for your service to function. Regularly review data storage and retention policies. ## Common security risks in API development A large part of compliance is about avoiding common security issues. Resources such as the [OWASP API Security Top 10](https://owasp.org/API-Security/editions/2023/en/0x11-t10/) provide a good starting point for understanding potential threats. Here are some common security risks to watch out for: ### Broken object level authorization This occurs when APIs don't properly verify that the requesting user has permission to access or modify a specific resource. For example, without adequate verification, a user might be able to access another user's data by manipulating an ID in the request URL. Prevent this by implementing proper access controls and authorization checks at the object level, such as verifying that the requesting user has the necessary permissions to access the requested resource. Row-level security can be implemented in the database or at the application level, depending on the use case. ### Broken authentication Authentication mechanisms that are implemented incorrectly may allow attackers to impersonate users or bypass authentication entirely. Prevention strategies: - Use standard authentication frameworks rather than custom implementations. - Implement proper token validation, revocation, and expiration. - Enforce strong password policies and multi-factor authentication. ### Broken object property level authorization Further vulnerabilities (formerly known as the "Excessive Data Exposure" and "Mass Assignment" security vulnerabilities) occur when APIs expose sensitive properties or allow properties that should be restricted to be updated. Prevention strategies: - Restrict responses by using explicit schemas to control what APIs return. - Restrict inputs by allowlisting certain properties for updates. ### Unrestricted resource consumption APIs that don't properly limit resource usage may allow attackers to launch denial-of-service attacks or cause excessive operational costs. Prevention strategies: - Implement rate limiting and quotas. - Set maximum sizes for payloads, pagination limits, etc. - Monitor and set alerts for unusual usage patterns. ### Broken function level authorization This occurs when APIs don't properly restrict access to certain functionality based on user roles or permissions. Prevent broken function level authorization by implementing RBAC and ensuring each API endpoint checks the users' permissions before executing requested actions. ### Unrestricted access to sensitive business flows APIs that offer unrestricted access allow for the automated abuse of business functionalities, such as ticket scalping, mass account creation, or content scraping. Prevention strategies: - Implement CAPTCHA or anti-automation for sensitive flows. - Apply business-specific rate limits. - Monitor unusual patterns of behavior. ### Server-side request forgery Vulnerabilities may allow attackers to make an API server send requests to unintended destinations, potentially accessing internal services. Prevention strategies: - Validate and sanitize all input. - Use allowlists for external service calls. - Implement network-level protections, like firewalls. ### Security misconfiguration Insecure default configurations, incomplete setups, and exposed cloud storage all put your API at risk. Prevention strategies: - Remove debug endpoints in production. - Keep dependencies up to date. - Use secure defaults for all configurations. ### Improper inventory management This occurs due to poor visibility into which APIs are deployed, including outdated, unpatched, or undocumented endpoints. A good service catalog can help you keep track of all your APIs, including their purpose, owner, and compliance status. [Kong's Service Catalog](https://docs.konghq.com/konnect/service-catalog/) is a good example of a tool that can help you manage your API inventory. ### Unsafe consumption of APIs This vulnerability occurs when your API consumes and processes data from third-party APIs without proper validation. Prevention strategies: - Validate all data from third-party APIs. - Implement timeouts and circuit breakers. - Apply the same security controls to third-party data as user input. ## Practical approaches to API compliance Let's explore proven, real-world methods that organizations are successfully implementing today to address compliance requirements in their API development. ### Using a modern API gateway Modern API gateways like Kong, Apigee, and AWS API Gateway provide built-in compliance tools. By centralizing compliance controls in the gateway, you can enforce policies consistently across all APIs and reduce the burden on individual services. This also simplifies audit preparations by providing a single point of control. The key features of modern API gateways include: - **Request/response transformation**: Remove sensitive data before it leaves your network. - **Rate limiting**: Prevent abuse and protect against DDoS attacks. - **IP restriction**: Whitelist trusted IP ranges. - **Key authentication**: Secure your APIs with API keys. - **ACLs**: Control access based on user roles. - **Logging**: Centralize access logs for auditing. ```yaml # Example Kong plugin configuration for PCI DSS compliance plugins: - name: request-transformer config: remove: headers: ["authorization"] - name: response-transformer config: remove: json: ["$.card.number", "$.card.cvv"] - name: rate-limiting config: second: 5 minute: 60 - name: ip-restriction config: whitelist: ["192.168.1.1/24", "10.0.0.1/16"] - name: key-auth config: key_names: ["api-key"] - name: acl config: whitelist: ["admin", "payment-service"] - name: log config: service_token: "log-token" log_level: "info" ``` This approach provides: - Centralized policy management - Consistent enforcement across services - Automated testing of policies - Clear audit trails ### Policy as code Policy-as-code tools like Open Policy Agent (OPA) allow you to define compliance policies as code that can be versioned, tested, and automated. The policies are written in a declarative language called Rego, which is designed to remove ambiguity and make policies machine-readable. By removing policy enforcement from the application code, you can ensure that compliance requirements are consistently enforced across all services and updated independently of the application code. ```rego # Example OPA policy for GDPR compliance package gdpr # Rule to check if a request requires explicit consent requires_consent { input.resource.type == "user_data" data.sensitive_fields[_] == input.action.field not input.subject.has_consent } # Rule to enforce data deletion (right to be forgotten) allow_deletion { input.action.type == "delete" input.resource.owner == input.subject.id } # Rule to check if data minimization principles are followed violates_data_minimization { input.action.type == "collect" not data.required_fields[input.action.field] } ``` This approach provides: - Centralized policy management - Consistent enforcement across services - Automated testing of policies - Clear audit trails ### Compliance-focused API testing Automated testing designed for compliance can catch issues early. ```javascript // Example Jest test for HIPAA compliance describe('HIPAA Compliance Tests', () => { test('PHI is encrypted in transit', async () => { const response = await fetch('https://api.example.com/patient/123', { headers: { 'Authorization': 'Bearer test_token' } }); // Check that the connection used TLS 1.2 or higher expect(response.connection.protocol).toMatch(/TLSv1\.[23]/); }); test('Access logs are created for PHI access', async () => { await fetch('https://api.example.com/patient/123', { headers: { 'Authorization': 'Bearer test_token' } }); // Check that access was logged const logs = await getAccessLogs(); expect(logs).toContainEqual(expect.objectContaining({ resource: '/patient/123', action: 'read', timestamp: expect.any(String), actor: expect.any(String) })); }); test('Session expires after inactivity', async () => { // Get token const token = await getAuthToken(); // Wait for token to expire await new Promise(resolve => setTimeout(resolve, 15 * 60 * 1000)); // Try to use token after timeout const response = await fetch('https://api.example.com/patient/123', { headers: { 'Authorization': `Bearer ${token}` } }); expect(response.status).toBe(401); }); }); ``` Add tests to your CI/CD pipelines to prevent non-compliant code from reaching production, and to catch issues early in the development process. ### Data catalogs and classification Modern data governance platforms like [Collibra](https://www.collibra.com/), [Alation](https://www.alation.com/), and [Atlan](https://atlan.com/) help track data lineage and enforce classification policies. Data lineage is the process of tracking data from its origin to its destination. It helps organizations understand how data moves through their systems and ensures compliance with data handling policies. Data catalogs are particularly useful in the modern AI-driven world, where data is often shared across multiple services and systems. By classifying data based on sensitivity and compliance requirements, you can ensure that only authorized services have access to sensitive data. Aimed at data stewards and compliance officers, these platforms provide: - Automated data discovery - Data classification and tagging - Data lineage tracking - Compliance reporting ### Compliance scanning in CI/CD Tools like [Snyk](https://snyk.io/), [Checkmarx](https://checkmarx.com/), and [SonarQube](https://www.sonarsource.com/products/sonarqube/) can be configured to scan for compliance issues during the build process: ```yaml # Example GitLab CI configuration stages: - test - compliance - deploy compliance_scan: stage: compliance script: - checkmarx-scan --preset "PCI-DSS" --source "${CI_PROJECT_DIR}" - owasp-dependency-check --project "API" --scan "${CI_PROJECT_DIR}" - sonar-scanner -Dsonar.projectKey="${CI_PROJECT_NAME}" -Dsonar.qualitygate.wait=true only: - main - staging ``` This ensures that compliance issues are caught before code reaches production, significantly reducing remediation costs. ## Implementation strategies A successful compliance strategy requires a combination of technical controls, developer education, and continuous monitoring. You can use the following practical steps to get started. ### Starting with a compliance matrix Begin by mapping your compliance requirements to specific technical controls, like in the example table below. | Requirement | Technical Control | Implementation | Verification Method | |--------------|--------------------------------|----------------------------------------|------------------------| | PCI DSS 4.1 | TLS 1.2+ for all transmissions | API Gateway HTTPS enforcement | Automated TLS scanning | | GDPR Art. 17 | Right to erasure API | `/users/{id}` DELETE endpoint | Integration tests | | SOC 2 CC7.2 | Comprehensive audit logging | Centralized logging with ElasticSearch | Log completeness tests | ### Practicing continuous compliance Rather than treating compliance as a point-in-time auditing activity, integrate it into your development process as follows: 1. **Design phase**: Include compliance requirements in acceptance criteria. 2. **Development**: Use prebuilt compliant patterns from internal libraries. 3. **Testing**: Run automated compliance tests and functional tests. 4. **Deployment**: Use infrastructure as code with compliance guardrails. 5. **Monitoring**: Set up alerts for compliance anomalies. ### Investing in developer education Create clear guidelines and training for developers: - Maintain an internal knowledge base of compliance requirements. - Develop design patterns and code examples for common compliance scenarios. - Run regular workshops on regulatory changes and updates. - Create developer-friendly checklists for different compliance frameworks. ## Overcoming common challenges Expect pushback from developers and legal teams. Use lists like the one below to prepare for the common objections you may encounter and plan how you will address them. ### "Legal requirements are too vague for technical implementation" **Solution**: Create a cross-functional team of legal and technical experts to translate requirements into specific technical controls. Document these translations in a shared repository. ### "Compliance slows down development" **Solution**: Build a library of pre-approved, compliant components that developers can reuse. Integrate compliance testing into your CI/CD pipeline to catch issues early. ### "We can't keep up with changing regulations" **Solution**: Subscribe to regulatory update services and join industry groups. Implement a regular review cycle for compliance requirements and allocate time for updates in each sprint. ## Who's responsible? By clearly defining roles and responsibilities, you can ensure that compliance is everyone's job, not just the legal team's: ### Developer responsibilities The development team's responsibilities include: - Implementing technical controls correctly - Writing tests that verify compliance requirements - Reporting potential compliance issues - Understanding the "why" behind requirements ### Legal team responsibilities The legal team is responsible for: - Translating regulations into clear requirements - Providing guidance on edge cases - Keeping up to date with regulatory changes - Preparing for audits and documentation ### DevSecOps team responsibilities The responsibilities of the development, security, and operations team include: - Building compliance tooling and automation - Monitoring for compliance drift - Creating reusable compliant patterns - Training developers on compliance requirements ## Resources and further reading Here are some resources to help you discover more about API compliance: ### Official documentation - [The NIST Cybersecurity Framework](https://www.nist.gov/cyberframework) - [The official text of the GDPR](https://gdpr-info.eu/) - [The PCI DSS implementation guidelines](https://www.pcisecuritystandards.org/) ### Tools - The [Kong API Gateway](https://konghq.com/), which you can use for centralized API governance. - The [Open Policy Agent](https://www.openpolicyagent.org/), which you can use for policy as code. - The [HashiCorp Sentinel](https://www.hashicorp.com/sentinel), which you can use for infrastructure policy enforcement. ### Guides and best practices - [The OWASP API Security Top 10](https://owasp.org/API-Security/editions/2023/en/0x11-t10/) - [The Google Cloud Well-Architecture Framework's guidelines for meeting compliance and privacy needs](https://cloud.google.com/architecture/framework/security/meet-regulatory-compliance-and-privacy-needs) - [The Security Pillar of the AWS Well-Architected Framework](https://docs.aws.amazon.com/wellarchitected/latest/security-pillar/welcome.html) By adopting these practical approaches to API compliance, you can satisfy legal requirements without sacrificing development velocity. The key is to integrate compliance into your everyday workflows rather than treating it as a separate after-the-fact activity. This not only reduces your risks and costs but builds trust with your customers and partners. Remember: The most effective compliance strategy is one that works with your development process, not against it. # Caching API Responses Source: https://speakeasy.com/api-design/caching import { Callout } from "@/mdx/components"; API caching can save servers some serious work, cut down on costs, and even help reduce the carbon impact of an API. However, it is often considered an optimization rather than what it truly is: an integral part of API design. A fundamental part of REST is APIs declaring the "cacheability" of resources. When working with HTTP there are many amazing caching options available through HTTP Caching; a series of standards that power how the entire internet functions. This can be used to design more useful APIs, as well as being faster, cheaper, and more sustainable. ## What is HTTP caching? HTTP caching tells API clients (like browsers, mobile apps, or other backend systems) if they need to ask for the same data over and over again, or if they can use data they already have. This is done with HTTP headers on responses that tell the client how long they can "hold onto" that response, or how to check if it's still valid. This works very differently from server-side caching tools like Redis or Memcached, which cache data on the server. HTTP caching happens on client-side or on intermediary proxies like Content Delivery Networks (CDNs), acting as a proxy between the client and the server and storing responses for reuse whenever possible. Think of server-side caching as a way to skip application work like database calls or outgoing HTTP requests, by fetching precomputed results from Redis or Memcached. HTTP caching reduces traffic and computational load further, by reducing the number of requests that even reach the server, and by reducing the number of responses that need to be generated. ## How does it work? HTTP caching is driven by cache headers. In its most simple form, when an API sends a response, it includes instructions that tell the client and other network components like CDNs if they are allowed to cache the response, and if so for how long. The guide on [API responses](/api-design/responses) briefly introduced the `Cache-Control` header: ```http HTTP/2 200 OK Content-Type: application/json Cache-Control: public, max-age=18000 { "message": "I am cached for five minutes!" } ``` Here the server is telling the client (and any cache proxies) that they can cache this response for 5 minutes, and they can share it with other clients too. This means that a client can use this data for up to 5 minutes without checking back with the server, and when that time has expired it will make a new request. Fetching data, processing it, and sending it back to the client takes time and resources. Even when all of those processes are as optimized as possible, if the data hasn't changed, why bother repeating these requests? Instead of wasting resources answering the same requests over and over again, the server could be processing more useful requests, saving energy, and save money by scaling down unnecessary server capacity. ### Cache-Control Defined in [RFC 9111: HTTP Caching](https://www.rfc-editor.org/rfc/rfc9111), this header sets out the rules. It tells clients what to do with the response: - `Cache-Control: max-age=3600` — The client can use this data for up to an hour (3600 seconds) without checking with the server. - `Cache-Control: no-cache` — The client must check with the server before using the cached copy. - `Cache-Control: public` or `private` — Defines whether just the client or everyone (like proxies) can cache it. These directives can be combined in various combinations for more control, with handy advanced options like `s-maxage` for setting how long data should live on shared caches like CDNs. Some simple APIs will only use `Cache-Control` to manage caching, but there's another powerful tool in the cache toolbox: `ETag`. ### ETag ETags (short for "Entity Tags") are like a fingerprint for a particular version or instance of a resource. When the resource changes, the ETag will change. No two versions of a resource should have the same ETag, and the ETag is unique to the URL of the resource. When a server sends a response, it can include an ETag header to identify that version of the resource: ```http HTTP/2 200 OK Content-Type: application/json ETag: "abc123" { "message": "Hello, world!" } ``` Then when a request is reattempted for whatever reason, the client sends a request with the ETag in the `If-None-Match` header. Doing this basically says "Only download the response if the ETag is different to this". ```http GET /api/resource HTTP/2 If-None-Match: "abc123" ``` - If the server responds with `304 Not Modified`, it tells the client, "That response is still good. Nothing has changed since then, so no need to download it again." - If the data has changed, the server returns the new data with a new ETag. This is especially helpful for large responses that don't change often, especially when combined with `Cache-Control`. Sending `Cache-Control` and `ETag` lets the client confidently reuse the data for a while without even needing to send a HTTP request to the server, then after that time it can switch to doing a check for changes instead of downloading the whole response again. All of this is done without the client needing to know anything about the data, or how it's stored, or how it's generated. The server will handle it all, and the client will just keep requesting the data, allowing the cache-aware HTTP client to do the heavy lifting. ## Using Cache-Control and ETags in code Let's add these headers to a basic Express.js API to see how it might look on the server-side. ```js const express = require("express"); const app = express(); app.get("/api/resource", (req, res) => { const data = { message: "Hello, world!" }; // Simulated data const eTag = `"${Buffer.from(JSON.stringify(data)).toString("base64")}"`; if (req.headers["if-none-match"] === eTag) { // Client has the latest version res.status(304).end(); } else { // Serve the resource with cache headers res.set({ "Cache-Control": "max-age=3600", // Cache for 1 hour ETag: eTag, }); res.json(data); } }); app.listen(3000, () => console.log("API running on http://localhost:3000")); ``` The ETag is generated by hashing the data, then the server checks if the client has the latest version. If it does, it sends a `304 Not Modified` response, otherwise it sends the data with the `ETag` and `Cache-Control` headers. In a real codebase, would be doing something like fetching from a datasource, or computing something that takes a while, so waiting for all of that to happen just to make an ETag is not ideal. Yes, it avoids turning that data in JSON and sending it over the wire, but if the API is going to ignore it and send an `304 Not Modified` header with no response, the data was loaded and hashed for no reason. Instead, an ETag can be made from metadata, like the last updated timestamp of a database record. ```js const crypto = require('crypto'); function sha1(data) { const crypto.createHash('sha1').update(data).digest('hex'); } const trip = Trips.get(1234); const eTag = `"${sha1(trip.updated_at)}"`; ``` This example creates a SHA1 hash of the updated time, which will automatically change each time the record is updated. No need to specify the name of the Trip resource, or even mention the trip ID, because an ETag is unique to the URL and that is already a unique identifier. When working with resources that have their own concept of versioning, why not use that version number as an ETag instead of creating one from something else. ```js const trip = Trips.get(1234); const eTag = `"${trip.version}"`; ``` ```http HTTP/2 200 OK Content-Type: application/json ETag: "v45.129" ``` Regardless, ETags are brilliant and easy to reconcile. If clients don't use them, it doesn't have any effect, but if they do use a HTTP client with [cache middleware](https://apisyouwonthate.com/blog/http-client-response-caching/) enabled then both the client and the server can save a lot of time and resources. ## Public, private, and shared caches Using `Cache-Control` headers its possible to specify whether the response can be cached by everyone, just the client, or just shared caches. This is important for security and privacy reasons, as well as cache efficiency. - `public` — The response can be cached by everyone, including CDNs. - `private` — The response can only be cached by the client. - `no-store` — The response can't be cached at all. When a response contains an `Authorization` header, it's automatically marked as `private` to prevent sensitive data from being cached by shared caches. This is another reason to use standard auth headers instead of using custom headers like `X-API-Key`. ## Which resources should be cached? Some people think none of the data in their API data is cacheable because "things might change." It's rare that all data is so prone to change that HTTP caching cannot help. All data is inherently out of date before the server has even finished sending it, but the question is how out of date is acceptable? For example, a user profile is not likely to change particularly often, and how up to date does it really need to be? Just because one user changes their biography once in a year doesn't mean that all user profiles need to be fetched fresh on every single request. It could be cached for several hours, or even every day. When talking about more real-time systems, one common example is a stock trading platform. In reality, most trading platforms publish a new public price every 15 minutes. A request to `/quotes/ICLN` might return a header like `Cache-Control: max-age=900`, indicating the data is valid for 900 seconds. Even when clients are "polling" every 30 seconds, the network cache will still be able to serve the response for 15 minutes, and the server will only need to respond to 1 in 30 requests. Some resources might genuinely change every second, and depending on the traffic patterns network caching could still be helpful. If 1,000 users are accessing it simultaneously then network caching will help significantly reduce the load. Instead of responding to 1,000 individual requests per second, the system can reuse a single response per second. This would be a 99.9% reduction in server load, and a 99.9% reduction in bandwidth usage. A safe default for most data is to apply some level of `max-age` caching (such as 5 minutes, an hour, a day, or a week, before it needs to be refreshed) paired with an ETag to check for fresh data past that time if the response is large or slow to generate. The introduction of ETags to an API can increase confidence in using longer cache expiry times. ## Designing cacheable resources All new APIs should be designed with cachability in mind, which means thinking about how to structure resources to make them more cacheable. The changes needed to make an API more cacheable are often the same changes that make an API more efficient and easier to work with. ### Resource composition One of the largest problems API designers face is how to sensibly group data into resources. There's a temptation to make fewer resources so that there are fewer endpoints, with less to document. However, this means larger resources, which become incredibly inefficient to work with (especially when some of the data is more prone to change than the rest). ```http GET /invoices/645E79D9E14 ``` ```json { "id": "645E79D9E14", "invoiceNumber": "INV-2024-001", "customer": "Acme Corporation", "amountDue": 500.0, "amountPaid": 250.0, "dateDue": "2024-08-15", "dateIssued": "2024-08-01", "datePaid": "2024-08-10", "items": [ { "description": "Consulting Services", "quantity": 10, "unitPrice": 50.0, "total": 500.0 } ], "customer": { "name": "Acme Corporation", "address": "123 Main St", "city": "Springfield", "state": "IL", "zip": "62701", "email": "acme@example.org", "phone": "555-123-4567" }, "payments": [ { "date": "2024-08-10", "amount": 250.0, "method": "Credit Card", "reference": "CC-1234" } ] } ``` This is a very common pattern, but it's not very cacheable. If the invoice is updated, the whole invoice is updated, and the whole invoice needs to be refreshed. If the customer is updated, the whole invoice is updated, and the whole invoice needs to be refreshed. If the payments are updated, the whole invoice is updated, and the whole invoice needs to be refreshed. We can increase the cachability of most of this information by breaking it down into smaller resources: ```http GET /invoices/645E79D9E14 ``` ```json { "id": "645E79D9E14", "invoiceNumber": "INV-2024-001", "customer": "Acme Corporation", "amountDue": 500.0, "dateDue": "2024-08-15", "dateIssued": "2024-08-01", "items": [ { "description": "Consulting Services", "quantity": 10, "unitPrice": 50.0, "total": 500.0 } ], "links": { "self": "/invoices/645E79D9E14", "customer": "/customers/acme-corporation", "payments": "/invoices/645E79D9E14/payments" } } ``` Instead of mixing in payment information with the invoice, this example moves the fields related to payment into the payments sub-collection. This is not only makes the invoice infinitely more cacheable, but it also makes space for features that are often used in an invoice system like payment attempts (track failed payments) or partial payments. All of that can be done in the Payments sub-collection, and each of those collections can be cached separately. The customer data is also moved out of the invoice resource, because the `/customers/acme-corporation` resource already exists and reusing it avoids code duplication and maintenance burden. Considering the user flow of the application, the resource is likely already in the browser/client cache, which reduces load times for the invoice. This API structure works regardless of what the data structure looks like. Perhaps all of the payment data are in an `invoices` SQL table, but still have `/invoices` and `/invoices/{id}/payments` endpoints. Over time as common extra functionality like partial payments is requested, these endpoints can remain the same, but the underlying database structure can be migrated to move payment-specific fields over to a `payments` database table. Many would argue this is a better separation of concerns, it's easier to control permissions for who is allowed to see invoices and/or payments, and the API has drastically improved cachability by splitting out frequently changing information from rarely changing information. ### Avoid mixing public and private data Breaking things down into smaller, more manageable resources can separate frequently changing information from more stable data, but there are other design issues that can effect cachability: mixing public and private data. Take the example of a train travel booking API. There could be a Booking resource, specific to a single user with private data nobody else should see. ```http GET /bookings/1234 ``` ```json { "id": 1234, "departure": "2025-08-15T08:00:00", "arrival": "2025-08-15T12:00:00", "provider": "ACME Express", "seat": "A12" } ``` In order for a user to pick their seat on the train, there could be a sub-resource for seating: ```http GET /bookings/:my_booking_ref/seating ``` ```json { "my_seat": "A12", "available_seats": [ "A1", "A2", "A3", "A4", "A5", "A6", ... ] } ``` Creating the seating sub-resource like this will make a unique seating chart for every single user, because "all the seats" and "this users seat" have been mixed together. These responses could still be cached, but it would have to be a `private` cache because the generic information has been "tainted" with data unique to each user. 10,000 users would have 10,000 cache entries, and the chance/impact of them being reused would be rather small, so there isn't much benefit to filling the entire cache with all this. Consider breaking this down into two resources: - `GET /bookings/:my_booking_ref` - See booking details, including current seat. - `GET /trips/:trip_id/seats` - List seat availability on the train. - `PUT /bookings/:my_booking_ref` - Update booking (eg to reserve a seat). By moving the seat information to the booking resource, the seating availability becomes generic. With nothing personalized about it at all, the resource can be cached for everyone who is trying to book a seat on this train. There is no downside to caching this data, because it is the same for everyone. Even if it changes, it's easy to grab the latest data from the server and suggest the user select another seat if it's no longer available. This allows the seat availability to be cached for a long time, and only worry about refreshing the plan if the `PUT` request fails because a seat is no longer available. ## Content Delivery Networks (CDNs) HTTP caching works well when clients use it, and many do automatically, like web browsers or systems with caching middleware. But it becomes even more powerful when combined with tools like [Fastly](https://www.fastly.com/) or [Varnish](https://www.varnish-software.com/products/varnish-cache/). These tools sit between the server and the client, acting like intelligent gatekeepers: ![A sequence diagram showing a Client, Cache Proxy, and Server. A web request travels from client to proxy, then is sent on to the server, showing a "cache miss". The response then travels back from the server to the cache proxy, and then is sent to the client](/assets/api-design/httpcachemiss.png) ![A sequence diagram showing a Client, Cache Proxy, and Server. A web request travels from client to proxy, but does not go to the server, showing show a "cache hit". The response is served from the cache proxy to the client without involving the server](/assets/api-design/httpcachehit.png) Client-caching like this is certainly useful, but the real power of caching comes when API web traffic is routed through a caching proxy. Using hosted solutions like Fastly or AWS CloudFront, this could be a case of changing DNS settings. For self-hosted options like Varnish, instead of pointing DNS settings to a hosted solution somebody will need to spin up a server to act as the cache proxy. Many API gateway tools like Tyk and Zuplo have caching built in, so this functionality may already be available in the ecosystem and just need enabling. ## Save emissions (and money) with HTTP caching The Internet (and its infrastructure) is responsible for [4% of global CO2 emissions](https://www.bbc.com/future/article/20200305-why-your-internet-habits-are-not-as-clean-as-you-think), and with [83% of web traffic coming from APIs](https://www.akamai.com/newsroom/press-release/state-of-the-internet-security-retail-attacks-and-api-traffic), it becomes critical to consider the carbon impact of new APIs. Each unnecessary API request costs server resources, bandwidth, and energy. That energy comes with a carbon footprint, whether it's from a data center powered by renewable energy or not. ## Summary By reducing redundant requests, HTTP caching can: - Cut down on server load (lowering hosting costs). - Reduce network traffic (lowering bandwidth fees). - Minimize energy consumption (a win for the environment). Imagine millions of users no longer making unnecessary requests for unchanged data. Designing APIs to be cache-friendly from the start not only benefits the environment but also leads to faster, more efficient, and user-friendly APIs. It's a win-win: better performance for users, lower operational costs for providers, and a positive impact on the planet. Next time a new API is being designed, ask the question: How much of this data do I really need to serve fresh each time, and how much of this can be cached with a combination of `Cache-Control` and `ETag` headers? ## Further Reading - [MDN: HTTP Caching](https://developer.mozilla.org/en-US/docs/Web/HTTP/Caching) - [ETags: What are they and how to use them?](https://www.fastly.com/blog/etags-what-they-are-and-how-to-use-them) - [What is Cache Control?](https://www.fastly.com/blog/cache-control-wild) # Enforcing API consistency with a large team Source: https://speakeasy.com/api-design/consistency import { Callout } from "@/mdx/components"; In large organizations where multiple teams contribute to an API ecosystem, maintaining consistency can be a challenge. Without some sort of defined strategy, APIs may diverge in their style, error handling, naming conventions, security practices, or even call the same thing different names. All of this causes confusion for users of the API, and this confusion costs time and money as support staff need to answer questions, profitable integrations are delayed, or breakages occur due to inconsistent behavior. To ensure uniformity, organizations need a combination of structured guidelines, automated enforcement, centralized API gateways, and thorough review processes. ## Define an API style guide Ask 100 developers how to do something and you'll get 101 answers on the best way to do it, and this can become a real headache when building APIs. Instead of picking unique approaches to everything, and API style guide can be written as a foundation for standardization, ensuring that all teams follow a common approach when designing and building APIs. A good guide should clearly define naming conventions for endpoints, query parameters, and request/response properties, and require establish standards for structured things like [pagination](/api-design/pagination), [error handling](/api-design/errors), [collections](/api-design/collections), and [HTTP status codes](/api-design/status-codes). Authentication and security guidelines must be outlined, covering mechanisms like OAuth, API keys, and rate limiting, all of which should work the same across APIs wherever possible to allow code reuse and reduce the cognitive load. Additionally, the guide should include a versioning strategy to manage changes effectively and set documentation standards for OpenAPI descriptions to ensure completeness and clarity. These style guides are living documents that should be updated regularly to reflect changes in best practices and organizational requirements, and can be published internally or publicly to help other organizations who might like your style. Lots of companies have done this, including [Google](https://cloud.google.com/apis/design), and many more which can be found [here](https://apistylebook.com/design/guidelines/). ## Automated style guides Writing all these decisions down is a good start, but it's not enough. Humans make mistakes, misread things, and misremember things. As guides evolve and new advice is added, people are unlikely to come back and read the guide again and might not spot the new advice. API linting tools exist to help, with two popular tools being Spectral, vacuum, and the Speakeasy CLI. This will not only validate OpenAPI documents to make sure they're syntactically correct, but can also be programmed to enforce the advice set out in the style guide. This means that teams who have built an API can check it's ok before deploying it to production, and teams who are following the API design-first workflow can get real-time feedback on the API as it is still being defined, further saving time and money from being wasted coding something problematic. Spectral, vacuum and Speakeasy all automate a big chunk of the API style guide, making API design reviews considerably easier, letting the review focus on more complex issues like "is this the right way to solve the problem?" rather than "is this the right way to capitalize a property?". Having all APIs following the same automated style guide, all integrated into OpenAPI editors, code editors, and run again in CI/CD pipelines, teams can be certain that APIs are going to be consistent, and only get better over time. See what linting rules you can create using Speakeasy CLI's lint command. ## Leveraging API gateways for centralized functionality One way to remove discrepancies between APIs is to not have multiple APIs doing the same thing in the first place. API gateways like [Kong](https://konghq.com/), [Tyk](https://tyk.io/), [Express Gateway](https://www.express-gateway.io/), and [AWS API Gateway](https://aws.amazon.com/api-gateway/) play a crucial role in standardizing authentication and authorization policies, rate-limiting, traffic filtering, and network caching. Leaving these features to be implemented in multiple APIs can be tricky even when installing the same libraries. For example, while OAuth 2 is a standard and should be implemented the same way regardless of the software, two different APIs might be using two different versions of the Ruby on Rails OAuth 2 server Doorkeeper, and one might have fixed a bug that the other hasn't. Those two will vary again from another implementation written in another framework or another language. They also help maintain uniform logging and monitoring practices by centralizing API usage tracking and performance metrics. Additionally, gateways can facilitate request and response transformations, ensuring backward compatibility without requiring changes across multiple services. This is a helpful way to remove inconsistencies from APIs which are already in production without complicating the codebase. ## API design reviews The API Design Review is a core component of a broader API governance program, where stakeholders evaluate proposed API changes to ensure they align with the organization's architecture and ecosystem. This process typically involves a diverse group, including API designers, developers, technical writers, system architects, and governance teams, all working together to maintain consistency and quality across the API ecosystem Just like code reviews are now common on pull requests, design reviews allow API designers and developers to submit OpenAPI-based proposals for review either before any code is written, or at the same time as code is written. A dedicated review committee, including API architects and experienced developers, should evaluate proposals based on adherence to guidelines and best practices. The automated checks can be run with linting on these pull requests before manual reviews to catch common issues efficiently, then design review meetings provide a platform to further discuss key API decisions, trade-offs, and potential improvements. This catches all the things a linter cannot, like "is this the right name for this concept" or "this was just added to another API, can we reuse that?" ## Summary Achieving consistency across an API ecosystem in a large organization requires a combination of well-documented style guides, automated enforcement through linting, centralized functionality via API gateways, and a structured API design review process. Implementing all or some of these approaches should ensure that APIs remain scalable, maintainable, and high quality, providing a surprise-free experience for API consumers and hopefully making life easier for API producers as well. # API developer experience: polishing the API's public interface Source: https://speakeasy.com/api-design/developer-experience Developer experience is a buzz word and so it's prone to hyperbole. It's not the end all be all that some people on social make it out to be. Nor is it irrelevant like some detractors claim. It is one of many important aspects of building an API (or any developer product). It's the final coat of polish that makes the product easily usable by the developer community. Done well, it can be the difference between winning and losing an RFP, between a successful and unsuccessful PLG motion. For an understanding of the impact great developer experience can have, let's take a look at Stripe, the canonical example company that wrote the playbook. ## What Stripe got right: A case study in API excellence [Stripe's](https://stripe.com/) API sets the gold standard for developer experience because it makes life easy for developers at every touchpoint. In 2011, Stripe was the first to offer an API-first approach to payments, and its evident in the number of startups that have popped up aiming to be the "Stripe for X". Let's take a look at what Stripe does right. ### SDK libraries No one wants to manually write API requests from scratch. Providing well-maintained SDKs in popular programming languages like Python, JavaScript, and Go can save developers countless hours of having to write boilerplate code for the API. Because SDKs are written in the language that's targeted, they can be more idiomatic when it comes to type safety. An SDK also makes it easier to predict how an API will respond to a request since the response object is typed. Stripe offers well-maintained SDKs for major programming languages and frameworks, making it easy to integrate payments without having to reinvent the wheel. For example, here's how to create a payment intent using Stripe's Python SDK compared to making a raw API request:
```python filename="stripe_sdk_example.py" import stripe stripe.api_key = "sk_test_4eC39HqLyjWDarjtT1zdp7dc" intent = stripe.PaymentIntent.create( amount=1000, currency="usd", payment_method_types=["card"], ) ```
```python filename="stripe_api_example.py" import requests url = "https://api.stripe.com/v1/payment_intents" headers = { "Authorization": "Bearer sk_test_4eC39HqLyjWDarjtT1zdp7dc" } data = { "amount": 1000, "currency": "usd", "payment_method_types": ["card"] } response = requests.post(url, headers=headers, data=data) print(response.json()) ```
### Sandbox environments for testing Experimenting is the easiest way to understand a tool. With sandboxes, developers can test their integrations without worrying about rate limits, API call costs, or breaking anything. Stripe makes it effortless by providing [sandbox environments](https://docs.stripe.com/sandboxes) that mirror production. This allows teams to simulate real-world scenarios without touching real money. ### Offer developer-first documentation Stripe nailed documentation by making it more than just a reference guide. Their documentation isn't just a manual — it's an experience. With clear, concise explanations, real-world examples, and interactive API explorers, developers can go from zero to functional in minutes. The Stripe docs offer code snippets in multiple languages, ensuring that whether using Python, Node.js, or Ruby, having a starting point. To complete the loop, the code snippets reference the Stripe SDKs for the different languages. For example, need to process a payment? Stripe provides a simple copy-paste code snippet that works right out of the box: ```python import stripe stripe.api_key = "sk_test_4eC39HqLyjWDarjtT1zdp7dc" charge = stripe.Charge.create( amount=2000, currency="usd", source="tok_visa", description="Charge for test@example.com" ) ``` ## How to make an API like Stripe Here are some strategies to ensure an API is not just functional but feels intuitive, reduces friction, and empowers developers to achieve their goals quickly and effectively. ### Understand the users' needs Different APIs serve different user bases. For instance, a payments API like Stripe caters to a different audience compared to Spotify's API for music data. An enterprise team integrating a merchant payments API will have different needs than a solo developer building a music app with a single pricing tier. Understanding the audience is the first step to creating an API that meets their needs - and keeping it stable for them over time. Enterprise teams need long-term consistency with clear versioning, while solo developers need flexibility without unexpected breaking changes. ### Make onboarding and authentication frictionless The difference between a good and bad API often comes down to onboarding. Just ask [@levelsio](https://x.com/levelsio/status/1853774638473437451), who compared xAI and Google Gemini's APIs:
![levelsio tweet](/assets/api-design/twitter-levels-screenshot.png)
While xAI needed only an API key, Google Gemini required multiple sign-ups, portal logins, and complex installation steps. For enterprise teams, inconsistent authentication across endpoints or juggling multiple keys can be equally frustrating. The solution? Provide simple API key authentication where possible: ```python # Simple API key authentication import requests headers = {"Authorization": "Bearer YOUR_API_KEY"} response = requests.get("https://api.example.com/data", headers=headers) print(response.json()) ``` ### Provide tooling that fits developers' workflows A great API should integrate seamlessly into developers' existing workflows and tools. Start with a virtual testing environment where developers can experiment safely without affecting production data. Including a **Run in Postman** button lets developers instantly import the API collection and start testing with minimal setup. Well-maintained SDKs that are regularly updated based on developer feedback complete the toolkit. Add type safety features that enterprise teams need - strongly typed responses and request validation help prevent runtime errors and make integration more reliable. By offering these tools and maintaining them diligently, meeting developers where they are, reducing set-up friction, and allowing them to integrate into the API into their existing workflows effortlessly. ### Make the API self-service Developers don't want to jump through hoops to get started. The entire process from onboarding to key generation and integration should be self-service. Clear, public documentation and interactive playgrounds help developers explore the API without needing support. The key to a smooth self-service experience is continuous improvement. Update the documentation based on developer feedback, add examples for common use cases as they are discovered, and keep changelog entries clear and accessible. When breaking changes are unavoidable, provide comprehensive migration guides that walk developers through the transition. Enterprise teams especially benefit from comprehensive guides that help them navigate compliance requirements and security implementations. ### Keep the API stable and reliable **Stability is the foundation of trust between API providers and consumers.** If the API is constantly changing, developers will lose confidence in the platform since they can't rely on it to work consistently. Preserve backward compatibility whenever possible, and when breaking changes are needed, provide clear migration paths. Quick bug fixes show developers their time is valued and improves trust in the platform. **Reliability means different things to different users.** Enterprise teams need clear rate limits, consistent error messages, and long deprecation notices. Solo developers need quick bug fixes and responsive support. Document the rate limits clearly and provide status pages that show real-time API health. **Version the API thoughtfully.** When breaking changes are unavoidable, maintain support for older versions during a reasonable transition period. Stay connected to the developer community through support channels, documentation surveys, and forums — their feedback is the roadmap to improvement. Think of the dashboard as more than just a control panel, it's a trust-building platform that gives developers confidence in the API's reliability and shows commitment to their success. ### Above all: Choose boring technology (it works) The foundation of great developer experience is a well-designed, intuitive API. No amount of documentation or tooling can compensate for an overcomplicated API design. REST is _almost_ always the right choice for an API. While alternatives like [GraphQL](https://graphql.org/) offer powerful features, they also introduce complexity that most applications don't need. Unless there are specific use-cases that demand more flexibility, stick with REST - it's well-understood, widely supported, and gets the job done. Remember, most people don't need the engineering patterns used by tech giants. Start simple, and only add complexity when real usage demands it. The goal is to make life easier for developers, not to just to have fun playing around with the latest architectural patterns. However, sometimes, breaking conventions can dramatically improve developer experience. Discord's Rich Presence API is a great example:
![Discord Rich Presence](/assets/api-design/discord-rich-presence.png)
Instead of REST, they use RPC for real-time updates about user activity: ```python # using https://github.com/Senophyx/Discord-RPC import discordrpc rpc = discordrpc.RPC(app_id=) rpc.set_activity( state="`Coding`", details="Building a new API", ) rpc.run() ``` This deviation from REST makes sense because it solves a specific problem - real-time status updates - in a way that's both powerful and easy to implement. The key is that Discord broke convention to improve developer experience, not just to be different. Let's say an indie developer is building a game and wants to show the player's current status in Discord. In this case, using RPC makes more sense than REST because it's real-time and doesn't require polling. If Discord had stuck with REST, developers would have had to poll the API every few seconds to get the same functionality, which would be inefficient and annoying. The lesson? Keep it simple by default, but don't be afraid to break conventions when it genuinely makes life easier for developers. ## Final thoughts Building an API that developers love isn't about following every trend or implementing complex architectures. It's about understanding the users, making practical design decisions, and providing useful tools to help them build. By following these principles, an API will be something that developers actually want to use, leading to faster adoption and fewer support headaches. ### Further reading To learn more about API design, [check out our guide to API design best practices](/post/api-design). # How to document your API Source: https://speakeasy.com/api-design/documentation Developers are the principal adopters of API products. By writing developer-friendly documentation, you can retain users, attract investment, and stand out from your competitors. In this guide, we break down how to document an API effectively, covering documentation structure, ongoing maintenance, user experience, internal documentation, platform choice, and more. ## Structuring your API documentation Good API documentation is intuitive to navigate, as it follows a familiar format. Most API documentation includes the following essential sections: * **Getting started:** An overview of the API and a simple first request. * **Setup and authentication:** An explanation of how users can authenticate and configure access to the API. * **API reference:** A detailed description of the API endpoints, request parameters, and responses. ### Getting started The **Getting started** page summarizes the purpose of the API and directs the developer to important sections. It should be short and to the point. The goal is to help users make their first successful request as quickly as possible by providing the following: * A short introduction explaining what the API does. * A description of the environment or a brief example of an authentication and request. * Links to the setup section, the reference section, and any other important resources. The Paddle API [Overview](https://developer.paddle.com/api-reference/overview) page for the Paddle API demonstrates this structure. It begins with an introduction. ![Paddle API references](/assets/api-design/paddle-get-started-introduction.png) Then, there is a short description of the [base URLs](https://developer.paddle.com/api-reference/overview#base-url) for the sandbox and live environments. ![Paddle API Base URLs](/assets/api-design/paddle-api-reference-base-urls.png) Finally, the page provides [links to key resources](https://developer.paddle.com/api-reference/overview#explore-entities), such as API references, SDKs, and authentication guides. ![Paddle API Links to resources](/assets/api-design/paddle-get-started-links.png) ### Authentication Authentication is a critical part of any API, and developers require a clear explanation of how they can authenticate their requests. Whether your API uses API keys, OAuth, or another method, this section should describe the following: * How users can obtain API credentials (for example, by completing a sign-up process and generating API keys. * How to use authentication tokens (such as in a header or query parameter). * Security considerations (including refresh strategies, token storage and expiration information, and whether a token can be used publicly). For example, view the [OpenAI Authentication page](https://platform.openai.com/docs/api-reference/authentication), which demonstrates these requirements by explaining where to create API keys, what their scopes are, how not to expose them, and where to include them in a request. ![CleanShot 2025-03-03 at 02.21.01@2x](/assets/api-design/openapi-api-authentication.png) ### API reference Your API reference forms the core of your documentation. It is typically generated from an OpenAPI specification (more on this later). This section should be structured for quick lookup and clearly provide the following details for all endpoints: * The method and URL (for example, `GET /orders`). * The parameters (such as `path`, `query`, `body`, and `headers`). * An example request (with real values, not placeholders). * An example response (including both a success and a failure case). * Error codes (with explanations). The documentation for the Stripe endpoint used to [Create an event destination](https://docs.stripe.com/api/v2/core/event_destinations/create) is a great example of an API reference. It clearly demonstrates the parameters of the request and provides an example request and response. ![Stripe API Create an Event](/assets/api-design/stripe-api-create-event.png) ### Additional sections The **Getting started**, **Authentication**, and **API references** sections form the integral parts of your API documentation. Whether you include other sections is up to you. However, if you want your documentation to stand out, we recommend you add the following pages: * **Usage limits:** Explain rate limits, pagination constraints, and whether certain requests support idempotency. * **Versioning:** Outline how you handle versioning. This may not seem important at launch, but when a major update occurs, a clear versioning strategy becomes essential. * **Table of contents:** Include a clear table of contents and strong cross-references between related documents (linking, for example, an "Order creation" guide to the relevant API reference). This helps developers quickly find what they need and improves the SEO and overall usability of your documentation. When it comes to documentation, too much text overwhelms developers, but too little text leaves them confused. You can use the following guidelines to balance conciseness with clarity: * For straightforward concepts, like authentication, your explanations should not exceed three paragraphs. * For more complex topics, like security challenges, rather focus on providing a clear outline and sufficient descriptions and demonstrations to ensure users' full understanding without unnecessary verbosity. ## Versioning your API Once your API is live, your documentation must evolve alongside it. Without a proper maintenance and versioning strategy, developers will struggle to keep up with changes, resulting in unnecessary frustration. ### Automate maintenance where possible If your API documentation integrates with OpenAPI, you can partially automate maintenance. A well-documented API schema (such as a thorough OpenAPI document) allows you to configure automatic updates triggered by changes to your code. However, while these updates ensure your API reference pages remain accurate, they do not apply to the rest of your documentation. If you're using a hybrid approach that includes guides and explanations, you'll still need to update other sections manually when your workflows or concepts change. ### Version your documentation APIs evolve, and so should your documentation. It's essential to **indicate precisely which API version** your documentation describes. As long as users rely on older versions of your API, your **previous documentation should remain accessible**. There are two common strategies for hosting multiple API documentation versions: * **Subdomains:** You can host legacy versions on subdomains like `docs.v1.project.io`, while keeping latest version documentation at `docs.project.io`. * **Query parameters:** Alternatively, you can use a query parameter approach, such as `docs.project.io?version=v1`. Both methods allow users to access previous versions while ensuring the default view displays your latest documentation. For example, Stripe uses query parameters to display a different version of the documentation after you select the API reference. ![Stripe API Reference](/assets/api-design/stripe-api-versioning.png) ### Maintain documentation stability with CI/CD and linting When using an automated or hybrid approach to documentation, integrated linting and CI/CD checks allow you to ensure none of your documentation breaks during production. To prevent developer frustration due to malformed Swagger files or broken links, you should always: * Use linting tools to validate API documents. * Set up CI/CD pipelines to test documentation builds before pushing changes live. * Verify hyperlinks and cross-references to prevent broken documentation links. For example, you can run the Speakeasy [linting command](/docs/prep-openapi/linting) to detect OpenAPI document formatting errors and warnings before generating your SDKs. ### Keep a public changelog Developers use changelogs to track API updates, deprecated fields, and breaking changes. Publishing a record of your API changes prevents users from unknowingly using outdated features and missing new improvements. Your changelog should: * List every significant API update accompanied by its date. * Clearly mark deprecated fields and provide alternatives. * Separate breaking changes from non-breaking changes. We recommend using the [Keep a Changelog](https://keepachangelog.com/en/1.1.0/) format, which offers a well-structured and consistent approach to publicly recording your updates. ### Mark deprecated elements clearly Inadequately documenting your deprecated fields endpoints and fields creates confusion and frustration for developers. In your API reference, you should mark deprecated elements with a `Deprecated` or `Legacy` tag and provide a short explanation of the change: * Indicate when you will remove the endpoint or field, giving developers time to transition. * Suggest alternatives to the deprecated functionality. * Use visual cues like strikethroughs, warning icons, or color changes to highlight deprecated elements. You can take inspiration from the [Stripe API documentation](https://docs.stripe.com/api/sources). ![Stripe Deprecated Example](/assets/api-design/stripe-deprecated-example.png) Using OpenAPI Specification 3.0, you can add the `deprecated` annotation to [endpoints](https://swagger.io/docs/specification/v3_0/paths-and-operations/#deprecated-operations) and [parameters](https://swagger.io/docs/specification/v3_0/describing-parameters/?sbsearch=Deprecated#deprecated-parameters). However, only OpenAPI Specification 3.1.x supports the `deprecated` annotation of schemas. * If you deprecate an endpoint, mark the entire endpoint as deprecated by adding `deprecated: true` under the operation: ```yaml paths: /v1/orders: get: summary: Get all orders deprecated: true description: This endpoint is deprecated. Use `/v2/orders` instead. responses: "200": description: Successful response ``` * If you deprecate a query parameter but the endpoint is still valid, mark the parameter as follows: ```yaml paths: /v1/orders: get: summary: Get all orders parameters: - name: status in: query description: Filter orders by status. This parameter is deprecated. Use state instead. deprecated: true schema: type: string responses: "200": description: Successful response ``` * If you deprecate a schema field, first ensure you are using a 3.1.x version of the OpenAPI Specification, then mark the specific field inside the request or response body as deprecated: ```yaml components: schemas: Order: type: object properties: id: type: string legacyField: type: string deprecated: true description: This field is deprecated and will be removed in future versions. ``` Notice how each example includes a description that informs the reader of the removal date or version and, when applicable, specifies the replacement field or endpoint to facilitate a smooth transition. ## Best practices for user and developer experience There is a difference between well-structured API documentation and great API documentation. Use the following recommendations to create a seamless, intuitive experience that makes developers want to integrate and recommend your API. ### Focus on quality A clean OpenAPI document leads to better autogenerated docs, higher quality SDKs, and a specification that is more useful to the developers who rely on it for their own tools and integrations. If you use Speakeasy for SDK generation, a well-structured OpenAPI document ensures clean, idiomatic SDK code samples that reduce friction. You can improve your OpenAPI document using the Speakeasy [OpenAPI guide](/openapi), which covers everything from introductory concepts to the best practices for writing OpenAPI components. ### Include a dark mode option Dark mode is a must-have for the accessibility of modern documentation. Many developers prefer it for readability, especially when working for long hours. Most documentation tools, including Docusaurus, ReadMe, and Mintlify, support dark mode by default, so enabling it should be a standard practice rather than an afterthought. ### Support copy-paste code snippets Developers should be able to copy API request examples with one click. Including a copy-to-clipboard button in all code blocks prevents manual errors and accelerates integration. Most documentation platforms, such as ReadMe, Mintlify, and Docusaurus, support copy-paste snippets natively, so be sure to add this feature if you create a custom documentation site. ### Use SDK code samples instead of raw HTTP requests Autogenerated API documentation often relies on `cURL`, `fetch`, or `requests` examples of API calls, but developers **prefer SDKs** that abstract API calls into **clean, idiomatic functions**. SDK-based examples reduce the code developers need to write, allowing users to integrate APIs more quickly. When an SDK includes a copy-paste button, using the API becomes even faster. [Speakeasy](/docs/sdk-docs/edit-readme) automatically generates SDKs and SDK documentation from OpenAPI files, including SDK-based snippets of sample code that enhance the documentation. We offer integrations with [Scalar](/docs/sdk-docs/integrations/scalar), [ReadMe](/docs/sdk-docs/integrations/readme), and [Mintlify](/docs/sdk-docs/integrations/mintlify), reducing the effort required for documenting your SDKs. ### Offer multi-language code examples Not all developers use the same programming language. If your API supports multiple SDKs, we recommend you provide examples in Python, JavaScript, Go, Java, and other languages. For example, Stripe and Twilio enhance user and developer experience by allowing users to switch between languages in the documentation. ![Stripe language switching](/assets/api-design/stripe-language-switching.png) ### Add a search feature Developers rely on the search function to navigate documentation efficiently. If your platform doesn't have a built-in search feature, consider adding one of the following. * [Algolia](https://www.algolia.com/) is a market leader known for the speed and accuracy of its searches. It is free for open-source projects, but getting approval for free access can take time. * [Typesense](https://typesense.org/) is a self-hosted, open-source alternative to Algolia with great performance. It requires more setup and has a steeper learning curve but offers users full control over the search functionality. ### Implement detailed error messages Error messages should do more than state an issue - they should help developers resolve the problem by offering explanations and solutions. The Stripe [Error codes](https://docs.stripe.com/error-codes) page provides examples of detailed error messages that: * Describe why the error occurred. * Indicate what caused the error. * Provide clear steps to resolving the error (when possible). ![Stripe Errors Page](/assets/api-design/stripe-errors-page.png) ### Integrate a testing client You can improve the developer experience of your documentation by integrating seamless API testing and authentication. We recommend that you: * **Enable shared authentication between your API dashboard and documentation:** This removes the need for developers to copy API keys manually, improves tracking across environments, and streamlines testing. While shared authentication requires additional development effort, the reduced friction makes it a valuable improvement. * **Allow API requests to be executed directly from the documentation:** Tools like Swagger UI provide basic request execution, while Scalar offers a more refined and configurable experience. * **Add an API playground for developers to experiment with live requests:** This makes it easier for developers to test different endpoints without additional setup and creates a more interactive and engaging documentation experience. ## Documenting internal APIs While much of this guide focuses on public-facing API documentation, internal APIs have their own documentation needs and considerations. Internal documentation doesn't need the same level of polish as public-facing docs, but it still needs to be clear and useful for your team. ### Simpler requirements for internal documentation Internal API documentation can be more streamlined than public-facing docs for several reasons: * Your audience already understands your company's domain language and technical context * You can make assumptions about your users' technical knowledge level * There's less need for extensive marketing content or competitive positioning For many teams, using something as simple as the raw protocol definition may be sufficient. Common approaches include: * **Protocol Buffers (protobuf)** for gRPC services * **OpenAPI documents** for REST APIs * **GraphQL schemas** for GraphQL APIs However, these raw specifications aren't always the most ergonomic for developers to use. They provide complete technical details but lack the narrative and context that make documentation user-friendly. ### Enhancing internal documentation Even for internal APIs, adding a layer of polish can significantly improve developer productivity and reduce onboarding time. Consider implementing: * **A lightweight documentation portal** using tools like Scalar, which can add a clean UI on top of your OpenAPI documents without requiring significant effort * **Context and explanations** around endpoints, explaining the business purpose rather than just the technical details * **Usage examples** that demonstrate common integration patterns specific to your internal systems * **Authentication workflow guides** tailored to your company's identity management system ### Internal documentation best practices When documenting internal APIs, prioritize: * **Keeping documentation close to code** to make updates easier and more consistent * **Documenting "why" not just "how"** to help developers understand the business context * **Including team contact information** so developers know who to ask for help * **Maintaining a changelog** specific to internal consumers * **Simplifying authentication examples** using internal tokens or service account patterns * **Setting up a documentation CI/CD pipeline** to ensure docs stay in sync with code ### Tooling for internal documentation For internal APIs, lightweight tools are often preferable: * **Scalar** provides a clean, modern interface for OpenAPI documents without requiring extensive setup * **Swagger** is probably the documentation tool most users are familiar with. It's a bit daedm but it's functional. * **Redocly** can be self-hosted for teams that need to keep documentation behind a firewall * **GitHub/GitLab Pages** with a simple static site generator can work well for teams with CI/CD pipelines Even for internal APIs, investing in good documentation pays dividends in developer productivity, reduced onboarding time, and fewer integration errors. ## Choosing your API documentation tools Selecting the right platform for creating, hosting, and maintaining your API documentation directly impacts developer experience, SEO, and scalability. The table below compares the leading documentation tools based on their strengths, standout features, and pricing. | Tool | Standout Feature | Best For | Strengths | Limitations | Starting Price | |------|------------------|----------|-----------|-------------|----------------| | [**Scalar**](https://scalar.com) | Interactive API client (downloadable as standalone app) | Developer-focused teams | Clean interface, framework integrations, great value | Limited MDX support, basic CI/CD | $12/user/month (Free for public APIs) | | **Mintlify** | Extensive theming & customization | Brand-conscious companies | Beautiful UI, MDX support, AI-ready documentation | Less robust API validation, higher price point | $150/month | | **Bump** | Performance with large APIs | Enterprise teams with complex APIs | Handles massive specs, excellent versioning, fast load times | Less polished UI, steeper pricing | $265/month | | **ReadMe** | Web-based visual editing | Non-technical teams | User-friendly for marketers and product managers, familiar interface | Limited automation, manual updates required | $99/month | | **Redocly** | Ecosystem integration | Teams using common frameworks | Built-in support in many frameworks, robust Git workflow | Dated UI, requires more customization effort | $10/user/month | ### Documentation approaches When selecting a platform, consider which approach fits your needs: 1. **Automated documentation:** Generate references directly from OpenAPI specs with minimal effort. 2. **Manual documentation:** Create narrative-driven docs with custom explanations and examples. 3. **Hybrid approach:** Combine automated references with handcrafted guides for the best developer experience. Your choice should align with your API's complexity, team composition, and available resources. Smaller APIs with technical users might do well with simple automated documentation, while customer-facing APIs benefit from more comprehensive solutions with robust customization options. ## Final thoughts Ultimately, the success of your API documentation comes down to the quality of your content and hosting. Everything else we cover in this guide serves as a boost to make your documentation stand out. Our core recommendations are straightforward: * **For an API with five or fewer endpoints:** Automate your reference documentation with Swagger or Scalar UI and include a basic introduction page and an authentication section. * **For a large or growing API:** Adopt a hybrid approach using both Docusaurus and Swagger to increase your control over the documentation while keeping it manageable, or if you can afford the additional cost, use a managed solution that offers more features, like ReadMe, Bump.sh, or Mintlify. * **For internal APIs:** Use lightweight solutions like Scalar that provide just enough polish on top of protocol definitions without requiring significant investment. Start simple, scale smartly, and always prioritize clarity over complexity. # Returning informative API Errors Source: https://speakeasy.com/api-design/errors import { Callout } from "@/mdx/components"; When building an API it's natural to put most of the focus into building a beautiful "happy path" where nothing goes wrong. Developers often don't like to consider the failure cases, because of course everything is going to work out just fine, so errors are often not designed with the same care as the rest of the API. Errors in an API are not just an edge-case, they are a crucial part of the functionality, and should be treated like a core feature to be proudly shared and documented with users. Failing clearly and concisely is arguably more important than any other aspect of API design. Errors should: - Be as detailed as possible. - Provide context as to exactly what went wrong, and why. - Help humans find out more information about the problem. - Help computers decide what to do next. - Be consistent across the API. ## HTTP Status Codes The journey to great errors starts with [status codes](/api-design/status-codes). Status code conventions exist to specify what category of error has occurred, and they are a great way to help developers make decisions automatically based on the status code, like automatically refreshing access tokens on a `403`, or retrying the request on a `500`. Learn more about [HTTP Status Codes](/api-design/status-codes), and how to use them effectively. ## Application Errors HTTP status codes only set the scene for the category of issue that has occurred. An error like `400 Bad Request` is generally used as a vague catch-all error that covers a whole range of potential issues. More information will be required to help developers understand what went wrong, and how to fix it, without having to dig through logs or contact the support team. Error details are useful for: 1. humans - so that the developer building the integration can understand the issue. 2. software - so that client applications can automatically handle more situations correctly. Imagine building a carpooling app, where the user plans a trip between two locations. What happens if the user inputs coordinates that which are not possible to drive between, say England and Iceland? Below is a series of responses from the API with increasing precision: ```http HTTP/1.1 400 Bad Request ``` A not very helpful error response, the user will have no idea what they did incorrectly. ```http HTTP/1.1 400 Bad Request "error": { "message": "Trip is not possible, please check start/stop coordinates and try again." } ``` This message could be passed back to the user which will allow them to figure out how to address the issue, but it would be very difficult for an application to programmatically determine what issue occurred and how to respond. ```http HTTP/1.1 400 Bad Request "error": { "code": "trip_not_possible", "message": "Trip is not possible, please check start/stop coordinates and try again." } ``` Now this includes data that can help our users know what's going on, as well as an error code which let's them handle the error programmatically if they would like to. So, we should always include both API error messages, as well as API error codes. Let's take a closer look at the best practices for each of these. ## API error messages API error messages should be clear, concise, and actionable. They should provide enough information for the developer to understand what went wrong, and how to fix it. Here are a few best practices for API error messages: - **Be Specific**: The error message should clearly explain what went wrong. - **Be Human-Readable**: The error message should be easy to understand. - **Be Actionable**: The error message should provide guidance on how to fix the issue. - **Be Consistent**: Error messages should follow a consistent format across the API. ## API error codes The use of an error code is well established in the API ecosystem. However, unlike status codes, error codes are specific to an API or organization. That said, there are conventions to follow to give error codes a predictable format. ![Screenshot of Stripe.com API documentation's "Error Codes" page, which explains how "error codes" are added to provide extra information on top of HTTP status codes.](/assets/api-design/stripe-error-codes.png) Stripe's error codes have a nice easy to understand structure. Each error has a code which is a string, and a message which is a string, and that string is documented online so it can be understood, or reported to support. ```http HTTP/1.1 400 Bad Request { "error": { "code": "trip_too_short", "message": "This trip does not meet the minimum threshold for a carpool or 2 kilometers (1.24 miles)." } } ``` This makes it easy for developers to react programatically to the error too: ```typescript if (error.code === 'trip_too_short') ``` ## Complete Error Objects Include a `code` and a `message` puts an error message off to a great start, but there's more to be done to turn errors into a handy feature instead of just a red flag. Here's the full list of what an API error should include: - **Status Code**: Indicating the general category of the error (4xx for client errors, 5xx for server errors). - **Short Summary**: A brief, human-readable summary of the issue (e.g., "Cannot checkout with an empty shopping cart"). - **Detailed Message**: A more detailed description that offers additional context (e.g., "An attempt was made to check out but there is nothing in the cart"). - **Application-Specific Error Code**: A unique code that helps developers programmatically handle the error (e.g., `cart-empty`, `ERRCARTEMPTY`). - **Links to Documentation**: Providing a URL where users or developers can find more information or troubleshooting steps. Some folks will build their own custom format for this, but let's leave that to the professionals and use existing standards: [RFC 9457 - Problem Details for HTTP APIs](https://www.rfc-editor.org/rfc/rfc9457.html). This is being used by more and more API teams. ```json { "type": "https://signatureapi.com/docs/v1/errors/invalid-api-key", "title": "Invalid API Key", "status": 401, "detail": "Please provide a valid API key in the X-Api-Key header." } ``` This example of an error from the [Signature API](https://signatureapi.com/docs/errors) includes a `type`, which is basically the same as an error code, but instead of an arbitrary string like `invalid-api-key` the standard suggests a URI which is unique to the API (or ecosystem): `https://signatureapi.com/docs/v1/errors/invalid-api-key`. This does not have to resolve to anything (doesn't need to go anywhere if someone loads it up) but it _can_, and that covers the "link to documentation" requirement too. ![API Documentation for the SignatureAPI, with an explanation of what the error is, what happened, and how to fix it](/assets/api-design/errors-documentation.png) Why have both a `title` and problem `detail`? This allows the error to be used in a web interface, where certain errors are caught and handled internally, but other errors are passed on to the user to help errors be considered as functionality instead of just "Something went wrong, erm, maybe try again or phone us". This can reduce incoming support requests, and allow applications to evolve better when handling unknown problems before the interface can be updated. Here's a more complete usage including some optional bits of the standard and some extensions. ```json HTTP/1.1 403 Forbidden Content-Type: application/problem+json { "type": "https://example.com/probs/out-of-credit", "title": "Not enough credit.", "detail": "The current balance is 30, but that costs 50.", "instance": "/account/12345/msgs/abc", "balance": 30, "accounts": ["/account/12345", "/account/67890"] } ``` This example shows the same `type`, `title`, and `detail`, but has extra bits. As per [RFC 9457](https://www.rfc-editor.org/rfc/rfc9457.html), the `title` (such as `"Not enough credit."`) should be the same for all problems with the same `type` (`"https://example.com/probs/out-of-credit"` in this example), while the `detail` can include information that is specific to the current problem occurrence. In this case, the `detail` shows the the attempted debit amount `50` that exceeds the current balance `30`. The `instance` field allows the server to point to a specific resource (or endpoint) which the error is relating to. Again URI could resolve (it's a relative path to the API), or it could just be something that does not necessarily exist on the API but makes sense to the API, allowing clients/users to report a specific instance of a problem with more information that "it didn't work...?". The `balance` and `account` fields are not described by the specification, they are "extensions", which can be extra data which helps the client application report the problem back to the user. This is extra helpful if they would rather use the variables to produce their own error messages instead of directly inserting the strings from `title` and `details`, opening up more options for customization and internationalization. ## Best Practices Handling errors in API design is about more than just choosing the right HTTP status code. It's about providing clear, actionable information that both developers, applications, and end-users of those applications can understand and act upon. Here are a few more things to think about when designing errors. ### 200 OK and Error Code HTTP 4XX or 5XX codes alert the client, monitoring systems, caching systems, and all sorts of other network components that something bad happened. **The folks over at CommitStrip.com know what's up.** ![This monster has got his API responding with HTTP Status 200 OK despite the request failing.](/assets/api-design/errors-200-ok.jpeg) Returning an HTTP status code of 200 with an error code confuses every single developer and every single HTTP standards-based tool that may ever come into contact with this API. now or in the future. Some folks want to consider HTTP as a "dumb pipe" that purely exists to move data up and down, and part of that thinking suggests that so long as the HTTP API was able to respond then thats a 200 OK. This is fundamentally problematic, but the biggest issue is that it delegates all of the work of detecting success or failure to the client code. Caching tools will cache the error. Monitoring tools will not know there was a problem. Everything will look absolutely fine despite mystery weirdness happening throughout the system. Don't do this! ### Single or Multiple Errors? Should an API return a single error for a response, or multiple errors? Some folks want to return multiple errors, because the idea of having to fix one thing, send a request, fail again, fix another thing, maybe fail again, etc. seems like a tedious process. This usually comes down to a definition of what an error is. Absolutely, it would be super annoying for a client to get one response with an error saying "that email value has an invalid format" and then when they resubmit they get another error with "the name value has unsupported characters". Both those validation messages could have been sent at once, but an API doesn't need multiple errors to do that. The error there is that "the resource is invalid", and that can be a single error. The validation messages are just extra information added to that single error. ```json { "type": "https://example.com/probs/invalid-payload", "title": "The payload is invalid", "details": "The payload has one or more validation errors, please fix them and try again.", "validation": [ { "message": "Email address is not properly formatted", "field": "email" }, { "message": "Name contains unsupported characters", "field": "name" } ] } ``` This method is preferred because it's impossible to preempt things that might go wrong in a part of the code which has not had a chance to execute yet. For instance, that email address might be valid, but the email server is down, or the name might be valid, but the database is down, or the email address is already registered, all of which are different types of error with different status codes, messages, and links to documentation to help solve each of them where possible. ### Custom or standard error formats When it comes to standards for error formats, there are two main contenders: #### RFC 9457 - Problem Details for HTTP APIs The latest and greatest standard for HTTP error messages. There only reason not to use this standard is not knowing about it. It is technically new, released in 2023, but is replacing the RFC 7807 from 2016 which is pretty much the same thing. It has a lot of good ideas, and it's being adopted by more and more tooling, either through web application frameworks directly, or as "middlewares" or other extensions. This helps avoid reinventing the wheel, and it's strongly recommended to use it if possible. #### JSON:API Errors [JSON:API](https://jsonapi.org/) is not so much a standard, but a popular specification used throughout the late 2010s. It focuses on providing a common response format for resources, collections, and relationships, but it also has a decent [error format](https://jsonapi.org/format/#errors) which a lot of people like to replicate even if they're not using the entire specification. #### Pick One There has been a long-standing stalemate scenario where people do not implement standard formats until they see buy-in from a majority of the API community, or wait for a large company to champion it, but seeing as everyone is waiting for everyone else to go first nobody does anything. The end result of this is everyone rolling their own solutions, making a standard less popular, and the vicious cycle continues. Many large companies are able to ignore these standards because they can create their own effective internal standards, and have enough people around with enough experience to avoid a lot of the common problems around. Smaller teams that are not in this privileged position can benefit from deferring to standards written by people who have more context on the task at hand. Companies the size of Facebook can roll their own error format and brute force their decisions into everyone's lives with no pushback, but everyone on smaller teams should stick to using simple standards like RFC 9457 to keep tooling interoperable and avoid reinventing the wheel. ### Retry-After API designers want their API to be as usable as possible, so whenever it makes sense, let consumers know when and if they should come back and try again., and if so, when. The `Retry-After` header is a great way to do this. ```http HTTP/1.1 429 Too Many Requests Retry-After: 120 ``` This tells the client to wait two minutes before trying again. This can be a timestamp, or a number of seconds, and it can be a good way to avoid a client bombarding the API with requests when it's already struggling. Learn more about [Retry-After on MDN](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Retry-After). Help API consumers out by enabling retry logic in Speakeasy SDKs. # A practical guide to exposing your API publicly Source: https://speakeasy.com/api-design/expose-api-publicly Exposing an API seems straightforward: Configure endpoints, add logging, and ensure your server handles traffic. In reality, the process is far more complex. Consider a SaaS API for payment processing. You could expose it and consider it done, but have you provided SDKs to help developers integrate it with their merchant platforms? Is your documentation clear on how to handle failed payments and retries? Have you planned for scalability during peak shopping periods? What developer support channels are available to troubleshoot payment issues? Are you compliant with industry regulations? There's no one-size-fits-all approach to exposing your API publicly as many of the decisions you make will be influenced by who your users are and which industry your API serves. The OpenWeatherMap API, for example, prioritizes open access while the Stripe API is gated with access controls. This guide outlines the core principles to consider before exposing your API publicly and provides a checklist to ensure your SDK is secure, scalable, and developer-friendly. We'll cover the essentials, like logging, scalability, monitoring, and SDKs, as well as the less-glamorous details that can make or break your API's success, like legal and compliance requirements and developer support. ## Exposing your API publicly: A checklist Here are the things you want to make sure you've taken care of before exposing your API publicly: - What is the URL structure of your API? - Is your API documented? - Have you provided SDKs for your API? - How will you maintain and scale your API? - Have you planned deployment strategies? - Do you have a versioning strategy in place? - Do you have an API gateway in place? - How will you monitor and improve your API's performance? - Does your API have the necessary observability in place? - How do you plan to communicate with and support your users? - Have you complied with all legal and regulatory requirements? - Do you have clear terms of service and acceptable use policies defined? - Does your API comply with data protection and privacy laws? ## Dedicated subdomain vs path-based APIs: Which is best for your API? The URL structure of your API affects its long-term scalability, security, and maintainability. Because this choice impacts deployment, security, and future growth, it's best to decide on a URL structure early in the development process. A dedicated subdomain (like, `api.example.com`) allows for a cleaner separation between your API and the frontend and makes it easy to configure independent caching, security policies, and DNS parameters. While a path-based URL structure (like, `example.com/api`) is simpler to deploy alongside your frontend, this approach requires more configuration and maintenance. Unless you're specifically trying to avoid CORS headaches or keep security and deployment straightforward, using a dedicated subdomain is the better choice. ## Document your API OpenAPI tools like Swagger and Redocly make API documentation easy to generate, but great documentation goes beyond automation. Companies like [Stripe](https://docs.stripe.com/api) set the standard with clear, developer-friendly API documentation - a factor that contributes to Stripe's success as a leading payment API. You don't need to have Stripe-level API documentation from day one, though. Start with clear, well-structured docs and improve over time. Here are our tips for valuable documentation: - **Use a good documentation standard:** OpenAPI is a common standard that helps structure your documentation with clear endpoint URLs, detailed schemas, and descriptive explanations to support developers integrating your API. Tools like FastAPI can further enhance your documentation by automatically generating interactive [Swagger UI or Redoc](https://fastapi.tiangolo.com/features/#automatic-docs) pages from your OpenAPI document. - **Make your API docs testable:** Allowing your developers to test some requests directly from the documentation saves them time. With Swagger UI, users can test a request with the **Try it out** button, like in this [Swagger Petstore API demo for the user login endpoint](https://petstore.swagger.io/#/user/loginUser). ![Testing user login](/assets/api-design/expose-api-publicly/testing-swagger.png) - **Describe schemas clearly:** Document property types well and provide examples of requests and responses, especially for complex endpoints. - **Provide sample apps or boilerplate code:** Help developers understand your API with real-world examples and provide working code they can copy or modify for easier integration. ## Provide SDKs for your API Software development kits (SDKs) enhance developer experience by simplifying API integration and abstracting raw HTTP requests across multiple programming languages. SDKs let developers focus on building their applications instead of dealing with API complexities. Two key aspects to focus on when building an SDK are: - **Language support:** Identify your target audience's most popular languages and prioritize SDKs for them. The most common languages to offer SDKs for are Python, JavaScript, and PHP. - **Documentation:** Include detailed guides and examples in your SDKs to help developers understand how to use them effectively. Once your API is public, optimize developer experience by identifying frequently used or complex endpoints and providing detailed SDK guides for them. Design your SDKs with easy updates and maintenance in mind. Minor updates and patches refine your API over time and your SDKs should be updated to reflect this and ensure compatibility with the latest programming language versions. Consider using [Semantic Versioning](https://semver.org/) for major, minor, or patch changes and keep a public, up-to-date `CHANGELOG.md` document. Open source your SDKs to allow for contributions from the developer community. ## Make your API maintainable and scalable For long-term success, your API must remain reliable and efficient as usage grows. It should maintain good response times under peak loads, allow for updates with zero downtime, and evolve without disrupting your users. Load testing simulates real-world traffic patterns to identify performance bottlenecks and ensure your API is scalable. > Short fact: The [Shopify team begins testing APIs many months before Black Friday](https://shopify.engineering/scale-performance-testing#) to ensure the platform is ready to handle over 2 billion database queries and 20 million webhooks per minute at peak traffic. Shopify's latest Black Friday report is available on its [interactive microsite](https://bfcm.shopify.com/). Key elements of successful load testing include: - **Selecting the right testing tools:** Open-source load-testing tool [JMeter](https://jmeter.apache.org/) supports a range of protocols, while [Locust](https://locust.io/) allows you to create custom load tests with complex user journeys and API interactions. If you already use an observability or monitoring tool, check whether it includes load-testing capabilities or offers recommendations. For example, if you're using Grafana, [k6](https://k6.io/) is easy to integrate. - **Planning a testing strategy:** Design test scenarios that simulate extreme conditions, such as a Black Friday peak when the system may need to handle billions of API requests in a single day. - **Collecting key metrics:** Monitoring response time, error rates, and resource usage helps identify system weaknesses and optimize performance. ## Plan deployment strategies Deploying API updates requires meticulous planning. Downtime during updates frustrates users, erodes trust, and damages your reputation. The right deployment strategy enables smooth transitions while maintaining availability. Some common deployment strategies to consider are: - **Blue-green deployment:** Maintain at least two environments, one live and one for staging. Deploy updates to staging, test thoroughly, and then switch traffic to the updated environment. Offering a sandbox environment can further help users test changes before they go live. - **Rolling updates:** Gradually replace old instances with new ones to reduce the impact of potential issues. - **Canary releases:** Deploy the updates to a small subset of users first, then monitor performance and usage before a full rollout. Regardless of the deployment strategy you opt for, be sure to communicate upcoming API changes to your users in advance and schedule maintenance for low-traffic hours. ## Implement a versioning strategy As your API evolves, updates should not disrupt existing integrations. Implement a robust versioning strategy to maintain backward compatibility while allowing for future improvements. The two main API versioning methods are: - **URL-based versioning:** The API's URL includes the version number by default, for example, `/api/v1/resource`. - **Header-based versioning:** Headers can be used to specify the API version without altering URLs, for example, `x-api-version`. API versioning best practices to keep in mind include: - Precisely document the changes in each version. - Communicate deprecation timelines to help developers prepare for transitions. - Maintain older versions for a defined period to allow users a smooth migration path. ## Use an API gateway Managing authentication, traffic control, and security is straightforward for a small API, but as adoption grows, these concerns can become operational bottlenecks. More clients increase the load and risk of abuse, new endpoints increase the security management overhead, and additional integrations introduce complexity, latency, and greater attack surfaces. An API gateway provides a centralized system for security, routing, and monitoring, making it essential for scaling an API. API gateways like Kong, AWS API Gateway, or Apigee enable functionality like: - **Rate limiting and throttling:** Protecting your API from abuse and DDoS attacks. - **Logging and monitoring:** Tracking request patterns, latency, and failures. - **Security filtering:** Blocking malicious traffic and enforcing access policies. [Kong](https://konghq.com/) stands out among API gateways for its performance, flexibility, and extensibility. It handles high traffic loads, works across multi-cloud environments, and offers a plugin system for custom needs - all without vendor lock-in. ## Develop a monitoring plan to optimize performance Once your API is exposed publicly, ongoing monitoring is critical to ensure reliability, detect issues early, and optimize performance. A solid monitoring plan prevents high latency, downtime, and security breaches. Key metrics to monitor include: - **Uptime:** Ensure your API is operational and accessible to users. - **Latency:** Measure response times to identify bottlenecks. - **Error rates:** Track HTTP response codes (like 4xx and 5xx errors) to identify failing endpoints. - **Traffic trends:** Monitor request volumes to detect anomalies like DDoS attacks or crawlers and help you prepare for scaling needs. - **Crashes:** Find and fix API failures quickly. Set up an alert system to respond to problems quickly. - **Token leaks:** Use uniquely prefixed API keys (for example, `ACMEAPITOK-AB12345-123123132-12313123`) to allow for automated token scanning across repositories (like [GitHub secret scanning](https://docs.github.com/en/code-security/secret-scanning/secret-scanning-partnership-program/secret-scanning-partner-program)) to detect leaks and warn users before potential abuse occurs. To monitor these metrics effectively, you can use self-hosted or third-party solutions, depending on your setup and resources: - **Self-hosted monitoring tools:** For full control over data, tools like [Prometheus](https://prometheus.io/) and [Grafana](https://grafana.com/) collect metrics and display them on a dashboard. - **Managed monitoring services:** If setting up and maintaining your own monitoring system isn't feasible, tools like [Sentry](https://sentry.io/welcome/), [Datadog](https://www.datadoghq.com/), and [Bugsnag](https://www.bugsnag.com/) require little setup and offer SDKs for various languages and frameworks. ## Develop an observability plan to enable a self-healing system Observability helps you understand not just what is broken but why and where issues occur. While monitoring detects problems like high latency on an endpoint, observability allows you to trace the root cause, such as a slow database query. Effective observability focuses on three key data types: - **Logs:** Like a diary for your system, logs record every event (for example, "Your user tried to access endpoint X and got an error"). - \*_Metrics:_ Numerical data points that show how your system performs over time. - **Traces:** Step-by-step representations of the journey of a request through your system that give insight into where delays or errors occur (for example, "This API request spent 90% of its time waiting for a database response"). Observability enables self-healing systems by detecting failures in real time and triggering automated responses. For example, observability tools can identify failing services and automatically [reroute traffic](https://learn.microsoft.com/en-us/azure/architecture/patterns/circuit-breaker) to prevent cascading failures. Tools like Datadog and [OpenTelemetry](https://opentelemetry.io/) provide observability features that help you monitor and analyze your system. ## Establish channels for communication and developer support Clear and consistent communication is essential for API management. Developers rely on timely updates about bugs, breaking changes, and support. Poor communication can lead to frustration, reduced adoption, and churn. Keep users informed to build trust and maintain engagement. It’s important to keep developers informed of changes to your API so they can adapt smoothly. Follow these practices so updates are communicated clearly and early: - **Announce significant changes early** so developers have time to prepare. - **Offer a sandbox environment** so that developers can test your API safely. - **Keep your documentation up-to-date and build a self-help community.** Ensure developers have reliable ways to reach you by following these steps to build strong support channels: - **Provide multiple communication channels:** Your developers should be able to contact you via email from your website, using tools like [Chatwoot](https://www.chatwoot.com/), or by phone. Initially, some API developers prefer to use community solutions such as Slack or Discord. - **Cultivate developer forums:** Participating in platforms like Stack Overflow or Slack helps address issues in real time and builds trust. - **Maintain a public status page:** A real-time status page helps developers stay informed about the availability and health of your API. It should include: - Real-time monitoring of uptime, latency, and error rates. - Advance notification of scheduled maintenance. - Incident reports providing timelines and resolutions. - Historical uptime data for transparency. If you're building a status page for your API, take inspiration from [Revolut's public system status page](https://www.revolut.com/system-status/), which helps users monitor the platform's operational status and stay informed about any disruptions or technical issues affecting services. ![Revolut Status Page](/assets/api-design/expose-api-publicly/revolut-status-page.png) Alternatively, [Gatus](https://github.com/TwiN/gatus) is a free and open-source status page solution, while [Statuspage](https://www.atlassian.com/software/statuspage) and [Uptime](https://betterstack.com/uptime) from BetterStack offer free subscription tiers and require minimal setup. ## Ensure legal and regulatory compliance Addressing legal and compliance requirements serves two primary purposes: Protecting the API provider and protecting the API users. As the API provider, ensure you're protected by: - Establishing clear boundaries regarding API usage and liability. - Preventing unauthorized usage, abuse, and potential legal action. - Ensuring eligibility for enterprise-level deals that require compliance certifications. Protect your users by: - Safeguarding personal and sensitive data in compliance with privacy laws. - Being transparent about how you use and store data. - Ensuring fair and secure API access for all users. ### Define clear terms of service and acceptable use policies Your API's terms of service form a legally binding agreement defining how users can and cannot use your API to protect your business from misuse and legal liabilities. Terms of service should include: - **API usage rules:** Defining rate limits, usage restrictions, and fair use policies. - **Liability disclaimer:** Protecting you if the API fails or causes harm to a user's application. - **Termination rights:** Outlining the conditions under which API access can be revoked due to policy violations. - **Data handling:** Clarifying how user data is collected, processed, and stored. While you can generate terms of service documents using tools like Termly, it's wise to have a legal expert review your document to ensure compliance with local laws. Without solid terms of service, users can misuse your API, overload your systems, or even take legal action if the API doesn't work as expected. ### Define a compliance plan for data protection and privacy laws If your API processes sensitive data, compliance with privacy regulations is critical. Protection laws dictating how data is collected processed and stored vary by region and industry, so thorough research is needed to ensure you're compliant. Most data protection regulations - like [GDPR](https://gdpr.eu/what-is-gdpr/), [CCPA](https://oag.ca.gov/privacy/ccpa), and [HIPAA](https://www.hhs.gov/programs/hipaa/index.html) - focus on the same core principles: Securing and encrypting user data, informing users about how their data is collected and processed, and ensuring users' privacy rights are respected. However, compliance requirements vary depending on where your users are located: - In the U.S: - CCPA: Governs how businesses collect and share data on Californian residents. - HIPAA: Applies to healthcare data, ensuring its security and confidentiality. - In the EU: - GDPR: Requires strict data protection, transparency, and user consent. Key differences in these policies include how consent is obtained and region-specific compliance requirements. To remain compliant, you need to implement strong data encryption and anonymization practices, provide clear privacy policies, and obtain explicit user consent when necessary. Compliance certifications are often required to prove your API's security and reliability if you plan to work with large businesses or regulated industries. These certifications validate your data protection practices and operational processes. Some of the most widely recognized frameworks for API security and data protection include: - [SOC 2](https://www.imperva.com/learn/data-security/soc-2-compliance/): Confirms that your API meets security, availability, and confidentiality standards. - [ISO 27001](https://www.iso.org/standard/27001): An international standard for managing information security. - [PCI-DSS](https://www.pcisecuritystandards.org/): Required if your API handles payment card transactions. - [FedRAMP](https://www.fedramp.gov/): Necessary for providing services to U.S. government agencies. To ensure compliance, perform security assessments, collaborate with auditors, and highlight certifications in your API documentation to build trust with clients. # File Uploads Source: https://speakeasy.com/api-design/file-uploads File uploads can be confusing to work with at first because it takes a bit of a mental shift to think about. Firstly, a file is usually not just a file, it also has metadata needs to go with it and that can be hard to keep track of. Secondly, it is not really a file upload, simply a resource or collection of resources with a `Content-Type` of something other than the usual JSON or XML. ## URL design To visualize how file uploads could be designed into an API, let's see how images could be added for two different use-cases. A user could have an avatar sub-resource, which might look like this: ``` /users//avatar ``` This can then be uploaded and retrieved on the same URL, making it a consistent API experience with any other type of resource. Multiple images could be needed for product thumbnails, and that can be a sub-collection of the product. ``` /product//thumbnails ``` A collection of resources could be available, and a particular thumbnail could be retrieved or deleted using regular semantics like `GET` and `DELETE` on the particular resource URL. ``` /product//thumbnails/ ``` ## POST or PUT There is no particular [HTTP method](/api-design/http-methods) specific to file uploads, instead we use the appropriate hTTP method for the resource or collection being worked with. For the example of a single avatar for each user, the URL is already known, and it does not make any difference whether this is the first avatar they have uploaded, or they have remade the same request 10 times in a row after an intermitted internet connection messed up the first few. This should use a `PUT`, because that means "The end result should be this, regardless of what is there right now." ``` PUT /users//avatar ``` When working with a collection, the URL of the resource is not known until it has been created. For this reason a `POST` would be more appropriate. ``` POST /product//thumbnails ``` How these uploads work could vary depending on the use case, so let's look at the most popular methods. ## Different methods of file upload There are a few popular approaches to file uploads in APIs: 1. Uploading a file by itself, like adding an avatar for an existing user. 2. Uploading a file with metadata in the same request, like a video file with a title, description, and geodata. 3. Importing a file from a URL, like a user's avatar from Facebook. It's not entirely unreasonable to consider an API using all of these approaches for different use cases throughout the API depending on the specifics. Lets learn how these things work, and talk about when to use one over the other. ### Method A: Direct file uploads When no metadata is needed to be uploaded with a request, a direct file upload is beautifully simple. - Uploading a CSV of emails being imported to send a tree sponsorship email to. - A new logo for a funding partner. - A replacement avatar for a user profile. In all of these situations, the file is the only thing that needs to be uploaded and they also have a handy content type that can go right into the HTTP request to let the API know what's coming. ```http PUT /users/philsturgeon/image HTTP/2 Authentication: Bearer Content-Type: image/jpeg Content-Length: 284 ``` Any file can be uploaded this way, and the API can infer the content type from the `Content-Type` header. The API can also infer the user from the token, so the request does not need to include any user information. The API will then save the file, and return a response with a URL to the file that was uploaded. This URL can be used to access the file in the future, and can be used to link the file to the user that uploaded it. The response here will have a simple body: ```json { "url": "https://cdn.example.org/users/philsturgeon.jpg", "links": { "self": "https://example.org/api/images/c19568b4-77b3-4442-8278-4f93c0dd078", "user": "https://example.org/api/users/philsturgeon" } } ``` That `user` was inferred from the token, and the `url` is the resulting URL to the avatar that has been uploaded. Normally this would be some sort of Content Delivery Network (CDN) URL, but it could be a direct-to-S3 URL, or a URL to a Go service that handles file uploads. splitting file uploads to a separate service and hosting them elsewhere keeps the API server free to do more productive work than reading and writing files. ### Method B: Upload from URL Depending on how the client application works, uploading from a file might not be the preferred approach. A common pattern is mobile clients uploading user images directly from the photo libraries on the mobile device, and the web teams were pulling avatars from Facebook or Twitter profiles after they have done a "social login" flow. This is common because its harder for the web application to access the raw content of a file using just browser-based JavaScript. At some point a server needs to be involved to read that, so whether they have uploaded via cloudinary or some other upload service, the API server is going to need to take a URL and download the file. The same endpoint that handled the direct upload can serve this same logic, with the `Content-Type` header changed to `application/json` and the body of the request containing a URL to the file. ```http PUT /users/philsturgeon/image HTTP/2 Authentication: Bearer Content-Type: application/json { "url" : "https://facebook.com/images/dfidsyfsudf.png" } ``` The API will then download the file from the URL, save it, and return a response with a URL to the file that was uploaded. This URL can be used to access the file in the future, and can be used to link the file to the user that uploaded it. ```json { "url": "https://cdn.example.org/users/philsturgeon.jpg", "links": { "self": "https://example.org/api/images/c19568b4-77b3-4442-8278-4f93c0dd078", "user": "https://example.org/api/users/philsturgeon" } } ``` Supporting both might not be necessary, but if they are, just support both the image types needed and the JSON alternative of that. HTTP makes that incredibly easy to do thanks to being able to switch `Content-Type`. ### Method 3: Separate metadata resource The above examples are great for simple file uploads, but what if there is a need to upload metadata with the file? This is where things get a bit more complex. One approach would be multipart forms, but they're pretty complex to work with and not ideal for large files. If sending a massive video file, it's not ideal to have to send the title, description, and tags in the same request as the video file. If the video file upload fails, it would have to be re-uploaded with the video file and all of the metadata again. The way YouTube handles uploads via API are an interesting examples of splitting out metadata and a video file. They use a two-step process which focuses on metadata first, which allows for the metadata to be saved and the video can then be retried and uploaded without losing the metadata. The YouTube Data API (v3) approach to [Resumable Uploads](https://developers.google.com/youtube/v3/guides/using_resumable_upload_protocol) works like this. First, they make a POST request to the video upload endpoint with the metadata in the body of the request: ```http POST /upload/youtube/v3/videos?uploadType=resumable&part=snippet,status HTTP/1.1 Host: www.googleapis.com Authorization: Bearer Content-Length: 278 Content-Type: application/json; charset=UTF-8 { "snippet": { "title": "My video title", "description": "This is a description of my video", "tags": ["cool", "video", "more keywords"], "categoryId": 22 }, "status": { "privacyStatus": "public", "embeddable": true, "license": "youtube" } } ``` The response then contains a `Location` header with a URL to the video upload endpoint: ```http HTTP/1.1 200 OK Location: https://www.googleapis.com/upload/youtube/v3/videos?uploadType=resumable&upload_id=xa298sd_f&part=snippet,status,contentDetails Content-Length: 0 ``` Then to upload the video it's back to direct file uploads. The video file can be uploaded to the URL provided in the `Location` header, with the content type set to `video/*`: ```http PUT https://www.googleapis.com/upload/youtube/v3/videos?uploadType=resumable&upload_id=xa298sd_f&part=snippet,status,contentDetails HTTP/1.1 Authorization: Bearer AUTH_TOKEN Content-Length: Content-Type: video/mp4 ``` What's cool about this approach, is that URL _could_ be part of the main API, or it _could_ be a totally different service. It could be a direct-to-S3 URL, Cloudinary, or some other service that handles file uploads. Larger companies will be more prone to building a service to handle such files coming in, whilst smaller teams might want to keep things simple and let the API do the heavy lifting. The larger the file, the more likely it will be desirable to split that off, as having the API handle these huge files - even if the uploads are chunked - will keep the HTTP workers busy. Maintaining those connections might slow down a Rails-based API for a long time, for example, so having another service would help there. ## Best practices ### Check Content-Type and Content-Length It is worth noting that the `Content-Type` header is not always reliable, and should not be trusted. If expecting an image, check the first few bytes of the file to see if it is a valid image format. If expecting a CSV, check the first few lines to see if it is a valid CSV. **Never trust input.** The only thing worth mentioning on that request is the addition of `Content-Length`, which is basically the size of the image being uploaded. A quick check of `headers['Content-Length'].to_i > 3.megabytes` will let us quickly reply saying "This image is too large", which is better than waiting forever to say that. Sure, malicious folks could lie here, so backend code will need to check the image size too. **Never trust input.** Protecting against large files is important, as it can be a denial of service attack. If users are allowed to upload files, they could upload a 10GB file and fill up disk space. This is why it's important to check the size of the file before writing it to disk. To make sure it seems to be the right type, and to make sure it's not too large, read the file in chunks. This can be done with a simple `File.open` and `File.read` in Ruby, or similar in other languages. The file is read in chunks, and then written to a file on disk. This is a good way to handle large files, as it's not trying to load the whole file into memory at once. ```ruby def update if headers['Content-Type'] != 'image/jpeg' render json: { error: 'Invalid content type' }, status: 400 return end if headers['Content-Length'].to_i > 3.megabytes render json: { error: 'File is too large' }, status: 400 return end file = File.open("tmp/#{SecureRandom.uuid}.jpg", 'wb') do |f| f.write(request.body.read) end # Do something with the file end ``` ### Securing File Uploads Allowing file uploads can introduce all sorts of new attack vectors, so it's worth being very careful about the whole thing. One of the main issues with file uploads is directory traversal attacks. If users are allowed to upload files, they could upload a file with a name like `../../etc/passwd`, which could allow them to read sensitive files on the server. Uploading from a URL could allow for [Server-Side Request Forgery (SSRF)](https://owasp.org/API-Security/editions/2023/en/0xa7-server-side-request-forgery/) attacks, where an attacker could upload a file from a URL that points to a sensitive internal resource, like an AWS metadata URL, or something like `localhost:8080` which allows them to scan for ports on the server. The [OWASP File Upload Cheat Sheet](https://cheatsheetseries.owasp.org/cheatsheets/File_Upload_Cheat_Sheet.html) has a lot of good advice on how to secure file uploads, including: - Limiting the types of files that can be uploaded. - Limiting the size of files that can be uploaded. - Storing files in a location that is not accessible via the web server. - Renaming files to prevent directory traversal attacks. - Checking the file type by reading the first few bytes of the file. - Checking the file size before writing it to disk. - Checking the file for viruses using a virus scanner. ## Summary Think about what sort of file uploads are needed, how big the files are, where they're going, and what sort of clients will be using the API. The YouTube approach is a bit complex, but a combination of 1 and 2 usually take care of the job, and help avoid complicated multipart uploads. As always, build defensively, and never trust any user input at any point. # Filtering Collections Source: https://speakeasy.com/api-design/filtering-responses When building a REST API, the ability to filter potentially large collections of data is essential, and sorting can reduce a huge amount of work for both the client and server. ## What is Filtering? Filtering allows API users to request only the data they need, "filtering out" irrelevant things on the server-side instead of making them do it themselves on the client-side. Reducing the amount of data being returned and transferred reduces server resources and improves performance, which reduces carbon emissions and saves money. ## How to Filter The most straightforward way to filter resources is by using query parameters. These are appended to the URL to refine the results of an API request. For example: ```bash GET /products?category=books&price_lt=20 ``` In this case, the request filters products where the `category` is "books", and the `price` field is less than 20. The query string is easy for both the API designer and users to understand, making it a natural choice for filtering data. Naming conventions and deciding if or how to use operators will vary depending on the implementation, but there are a few common practices and standards to consider. ### Simple Filtering Starting with the most basic, filter by a single parameter using a query parameter with a sensible name. ```bash GET /products?category=books&status=available ``` In these examples, the query parameter `category` or `status` is used to remove any products that don't match those exact values. The query parameters in some APIs might be a little busy, as there could be not just sorting and pagination, but people do things changing output structures, selecting which properties should be returned, or all kinds of functionality which are not filtering. To avoid confusion, it's a good idea to use a consistent naming scheme, like `filter_category` or better yet a "filter array", e.g.: ```bash GET /products?filter[category]=books&filter[status]=available ``` This makes it clear that these are filtering parameters, keeping it separate from pagination, sorting, or any response modifiers which may be present. Sometimes, users want to combine multiple filters. This is generally done by adding more parameters to the URL: ```bash GET /orders?filter[status]=shipped&filter[customer_id]=123 ``` Using multiple filters is always considered a logical `AND` and the filters should be combined. Supporting a logical `OR` is trickier to represent in a query string, but one common convention is to allow multiple values for a single parameter with a comma-separated list: ```bash GET /products?category=books,electronics ``` This would return products in either the "books" or "electronics" categories. ### Declaring Operators Simple value matching is the most common form of filtering, but it might not be enough depending on the use-cases clients expect. For example, filtering for books with a price of `20` will ignore any books that cost `19.99`, which is probably not very helpful. ```bash GET /products?filter[price]=20 ``` To solve this, use operators to specify the type of comparison, like "less than", "greater than", or "not equal". These are usually implemented with suffixes or specific words added to the parameter name. For example, `GET /products?price_gt=50` would retrieve products where the price is greater than 50. Other common operators include: - `_lt` for less than (e.g., `price_lt=20`) - `_gt` for greater than (e.g., `price_gt=100`) - `_gte` and `_lte` for greater than or equal to, and less than or equal to, respectively. Some people are tempted to try and use operators as a prefix for the value, like `GET /products?price=<20` but that gets fairly awkward if trying 'less than or equal': `GET /products?price=<=20`, everything needs to be escaped, and its impossible to read. Sticking with the filter array approach, this can be made a little more readable: ```bash GET /products?filter[price][lt]=20 GET /products?filter[price][gt]=99 GET /products?filter[price][gte]=100 ``` This is a little more verbose, but it's much easier to read and understand. ### Advanced Filtering Instead of trying to invent a new approach, there are standards that can be used to make this easier for everyone, like [FIQL](https://datatracker.ietf.org/doc/html/draft-nottingham-atompub-fiql-00), [RSQL](https://github.com/jirutka/rsql-parser), or [OData](https://www.odata.org/getting-started/basic-tutorial/#queryData). As an example, OData is a widely used standard that provides a consistent way to query and manipulate data. It uses a specific syntax for filtering, which might look like this: ```bash GET /products?$filter=category eq 'books' and price lt 50 ``` Here, `$filter` is the standard keyword for filtering, and `eq` is used for equality, while `lt` means less than. You can combine multiple filters using `and`, just like in the example above. FIQL is a compact, text-based query language used for filtering. It uses operators such as `==` for equality, `!=` for not equal, `<` and `>` for less than and greater than, and `;` for AND logic. For example, a FIQL filter might look like this: ```bash GET /products?filter=category==books;price<20 ``` This is a concise way to express complex filtering logic, making it useful for more advanced APIs. Another option is RSQL, which is a slightly more modern version of FIQL that is gaining popularity: ```bash GET /products?filter=category==books,price<50 ``` RSQL uses a comma to separate filters, which is a little more readable than the semicolon and doesn't need to be URL encoded. It can make some amazing queries like `last_name==foo*,(age=lt=55;age=gt=5)`. Whichever of these formats is picked it will have pros and cons, but the most important thing is to pick a standard instead of reinventing the wheel, to leverage existing libraries and tools on both the client-side and the server-side. It's important to reuse existing tools for things like this instead of wasting infinite time building and maintaining custom solutions instead of solving genuine problems for users. ## What is Sorting? What order to return resources in a collection? - Oldest first or newest first? - Alphabetical based on the name? - Highest price to lowest price? Whichever is picked at first may be a sensible default, but it's likely that users will want to change this. For APIs, sorting is the process of arranging resources in a specific order based on user inputs. ## How to Sort Sorting is usually done with a query parameter: ```bash GET /products?sort=name ``` This sorts products by the `name` property, and by default that will be in ascending order. Most APIs will also allow clients to specify the order, which is usually done with another query parameter: ```bash GET /products?sort=price&order=desc ``` Here if we just had `sort=price` it would be reasonable to assume the client wanted the cheapest results, but if we're looking for the most expensive products, we can add `order=desc` to return the most expensive first. This convention is very closely related to the SQL `ORDER BY` clause, which takes a database property and an order in exactly the same way. Unlike a database query the API does not have to allow clients to sort by every single property, it could be restricted to a few common use-cases and make sure they are well optimized. ## Best Practices ### Consistency and Documentation When designing filters for the REST API, it's important to make sure they are intuitive and consistent. Use clear, descriptive names for the parameters. For example, `price_lt` is much easier to understand than something vague like `lower_price`. Providing solid documentation is equally important. Developers should be able to quickly find information on the available filters and how to use them. ### Validation and Error Handling Validation is also critical. If a user tries to apply a filter with invalid data (like `price=abc`), the API should return a helpful error message rather than just failing silently or returning incorrect results. Be sure to handle edge cases as well, such as empty values or invalid characters in the query string. _Learn more about [error handling in REST APIs](/api-design/errors)._ ### Performance Considerations The more that clients are allowed to customize their requests, the harder it becomes to set up caching rules and optimize database queries that might be produced. Anyone using an SQL database will know that the more complex the query, the harder it is to optimize. If clients are allowed to send in completely arbitrary queries, it's going to be very hard to optimize the database because it won't be clear what indexes to create. Being left retroactively optimizing for popular usages, which might be ok for an internal API used by a limited number of colleagues who can warn of popular usages, but is a nightmare for teams maintaining public APIs where an API could be brought down by a single user launching a new product. Rate limiting can help, but it's worth questioning: what is the purpose of this API? Generally an API is not meant to be a database-over-HTTP, so if the API design feels like it is starting to recreate SQL or some other query language, it might be going down the wrong path. There are databases which can be used over HTTP that do not require creation of a database, like FaunaDB, Firebase, or DynamoDB, which might be a better fit. ### URL Design Sometimes a filter could or should have been a different endpoint, a different parameter, or a different way of structuring the data. If the clients have asked for the ability to show off some "Hot Bargains", instead of telling clients to pick numbers based on price with `GET /products?price_lt=20&sort=price`, why not make it quicker and easier for everyone by creating `GET /products/bargains`. Cachability is improved, because you can set a 24 hour [network cache](/api-design/caching) on that which will be shared by all clients. Consistency is improved, because the web and iOS versions of the same application aren't going to pick slightly different numbers for what is considered a bargain. ## Summary Filtering is a powerful tool for API designers, allowing users to request only the data they need. By using query parameters, operators, and standard query languages, you can create a flexible and intuitive filtering system that meets the needs of your users, without going overboard and confusing everyone or making the API wildlife inefficient and unstable. When in doubt, start simple, and add things later. It's always easier to add new parameters, endpoints, and additional ways of doing things, than it is to take them away later. # api-design Source: https://speakeasy.com/api-design import { apiDesignSections } from "@/lib/data/api-design"; import { CardGrid, PageHeader } from "@/mdx/components"; import { TechCards } from "@/components/card/variants/docs/tech-cards"; import { WebGLVideo } from "@/components/webgl/components/video.lazy";
## API Design Basics Start with these foundational concepts to build a solid API.
## Advanced Considerations Once you've mastered the basics, these topics will help you build more sophisticated APIs.
## Productizing APIs Turn your API into a polished product with these guides on documentation, security, and more.
# Monetizing your API: Focus on flexibility for iterative pricing Source: https://speakeasy.com/api-design/monetization Anyone who has ever monetized an API will tell you: It's harder than you think. Monetization is an organization-wide project that affects the bottom line unlike anything else you'll build. This guide focuses on the early-stage technical billing decisions you can make to avoid painting yourself and your organization into a corner. Although we'll touch on pricing models and other strategic topics, this guide is intended for product teams and engineers and focuses on the technical aspects of monetizing an API. ## Anticipate change In the crunch-time rush to sign up your first enterprise customer, you may be tempted to assume a fixed pricing model. Building this pricing logic into your application is the critical mistake you want to avoid. Pricing changes over time. We're not talking only about inflation or the price of eggs (although inflation plays a role); we're referring to the inevitable and often rapid changes you should expect in market conditions. Anticipating rather than fighting these changes can bring sustained competitive advantage in the face of the wonderful surprises the market inevitably brings: Competitive pivots, rapid market entry, customer bargaining, pricing experiments, product expansion, and shifting cost structures will all demand iterative pricing changes. These principles are inspired by the five pillars of [PriceOps](https://priceops.org/) and long nights of patching home-grown billing systems. When API functionality and monetization are tightly intertwined, even modest changes to pricing models will require code refactoring, pulling engineers away from core product development, growing organizational friction, and making expensive rewrites the norm. ## Design principles that support pricing flexibility The engineering best practices that enable monetization flexibility revolve around decoupling, modularity, and the separation of concerns. In many ways, the following principles resemble good engineering practice in general, but we can't overstate their importance in pricing systems. ### Create a single source of truth for your immutable pricing definitions Your pricing definitions must exist in a single, authoritative database or system. Pricing plans and their definitions must be immutable; once set, they must never change directly. Instead, new variations should emerge as new, separately versioned plans so that existing customers retain their current versions of the pricing plan until they are explicitly moved. This principle ensures that new monetization plans or experimental tiers are added independently and safely, never affecting existing subscriptions or breaking customer configurations. ### Use modular entitlement checking that is decoupled from business logic Entitlements (a concept that will come up often in this guide) are the permissions granted to customers according to their subscription plans. You can think of entitlements as the features a customer is allowed to use based on their chosen plan or the usage they've accrued. You must never embed explicit plan or entitlement logic directly into your API business logic. Each request to your endpoints should merely check the customer's entitlement in a decoupled entitlement service layer or middleware component, which determines whether the customer can use a specific API feature by referencing your single source of pricing and entitlement truth. When you query customer plans dynamically, your pricing adjustments become simple changes to entitlement rules rather than risky API code changes. ### Centralize your pricing-agnostic metering and analytics Don't embed detailed metering logic directly into your application code. Instead, generalize the usage reporting to metering systems and ensure it is pricing agnostic. This reduces the load on developers and engineers by eliminating the need for new code in future pricing experiments or approaches and by enabling the reuse of historical metering records for launching new plans. Measure and record usage continuously, regardless of your current monetization model. If there is the slightest chance a user action can one day be monetized, its usage should be recorded. ### Create clear boundaries and responsibilities You should have clearly defined responsibilities and boundaries for your pricing data definitions, entitlement checks, and billing mechanics, as well as for your core API functionality. Ideally, each area should be owned by different teams and services, and use clean APIs and messaging patterns to communicate across boundaries so that transformation in one area occurs independently, without cascading changes to other areas. ## Designing your technical stack for monetization From the API service's perspective, there should be only two connections to monetization. ```mermaid flowchart TD client([Client]) gateway[API gateway] auth[Auth middleware] entitle[Entitlement middleware] ratelimit[Rate limiting] api[API service] meter[Metering service] client --> gateway gateway --> auth auth --> entitle entitle -->|"Entitlement check before execution"| api api -.->|"Metering during or after execution"| meter gateway --> ratelimit ``` This intentionally minimal coupling creates a clean separation between your core service functionality and monetization concerns. The API service only needs to know: 1. Whether the request is allowed to proceed (entitlement check before execution) 2. What usage to report once the request completes (metering after execution) Everything else (pricing plans, rate calculations, billing periods, invoicing, and subscription management) lives outside your core API code. This separation delivers several benefits: First, it enables your engineering and product teams to evolve core functionality independent of monetization strategies. Your API developers can focus on building features without worrying about pricing implications, while your product and business teams can experiment with pricing models without requiring engineering resources. Second, this separation minimizes the risk of critical errors during pricing model changes. When pricing logic is embedded throughout your codebase, seemingly innocuous changes to pricing can introduce subtle bugs that are difficult to detect until they impact customers. By isolating monetization to specific touchpoints, you contain both the risks and the testing surface area. Third, it creates a future-proof architecture that can adapt to evolving business models. As your product matures, you might transition from simple request-based pricing to more sophisticated models like outcome-based, value-based, or tiered pricing. With proper separation, these transitions become configuration changes rather than engineering projects. ## Step-by-step implementation There are several practical approaches to maintaining the separation between monetization and your API code. We recommend following a step-based approach, starting from your API service and working outwards, to ensure flexibility from day one. ### Step 1: Add entitlement checks as middleware Implement entitlement checking as true middleware that runs before your API controllers. This approach works well with gateway-based architectures, like Express, Django, FastAPI, and Spring Framework, which allow you to configure middleware chains. The middleware should: 1. Extract customer identification from the authenticated request. 2. Extract the feature identifier from the request. 3. Query your entitlement service to check whether the feature is allowed. 4. Reject unauthorized requests before they reach your business logic. Here's how this could work as a FastAPI middleware: ```python from fastapi import FastAPI, Request from fastapi.responses import JSONResponse from features import map_path_to_feature_id app = FastAPI() @app.middleware("http") async def check_entitlement(request: Request, call_next): # Step 1: Extract customer identification from the authenticated request. customer_id = request.state.user.customer_id # Assumes auth middleware sets this # Step 2: Extract the feature identifier from the request. feature_id = map_path_to_feature_id(request.url.path) # Step 3: Query your entitlement service to check if the feature is allowed entitled, message = await check_entitlement(customer_id, feature_id) # Step 4: Reject unauthorized requests before they reach your business logic if not entitled: return JSONResponse(status_code=403, content={"error": message}) # Request is authorized for this feature return await call_next(request) ``` This is overly simplified, so let's consider the strategies for handling complex entitlement checks. The FastAPI solution above becomes slightly complicated when the entitlement service requires other attributes (or worse, **derived** attributes) of the request. If, for example, your API has an endpoint that takes potentially large file uploads as input, you may wish to send the sizes of attached files to the entitlement service, along with the `customer_id` and `feature_id`, to test entitlement. Essentially, you would be asking something like, "Is customer 23 entitled to the upload image feature if the image is 32GB?" If computing the required context isn't computationally expensive, you could send the length of the request body and the file sizes along with your entitlement request for each request. For example: ```python from fastapi import FastAPI, Request from fastapi.responses import JSONResponse from features import map_path_to_feature_id @dataclass class EntitlementContext: file_sizes: Dict[str, int] body_size: int app = FastAPI() @app.middleware("http") async def check_entitlement(request: Request, call_next): customer_id = request.state.user.customer_id feature_id = map_path_to_feature_id(request.url.path) # Calculate the context of this request context = EntitlementContext( file_sizes={file.filename: len(file.body) for file in request.files}, body_size=len(await request.body()) ) entitled, message = await check_entitlement(customer_id, feature_id) if not entitled: return JSONResponse(status_code=403, content={"error": message}) return await call_next(request) ``` Calculating these sizes can be expensive. If this is the case, you might consider a more complex solution that uses the feature mapping function to return another function that, when called, calculates the context. This function can be passed to the middleware to calculate the context only when needed. ```python from fastapi import FastAPI, Request from fastapi.responses import JSONResponse from features import map_path_to_feature_id app = FastAPI() @app.middleware("http") async def check_entitlement(request: Request, call_next): customer_id = request.state.user.customer_id # map_path_to_feature_id returns a tuple of feature_id and a function that calculates the context feature_id, context_calculator = map_path_to_feature_id(request.url.path) if context_calculator: # Calculate the context of this request if needed. Push complexity to the feature mapping function. context = context_calculator(request) else: context = None entitled, message = await check_entitlement(customer_id, feature_id, context) if not entitled: return JSONResponse(status_code=403, content={"error": message}) return await call_next(request) ``` However simple your entitlement logic might be now, you should push any entitlement-checking complexity as far from your core business logic as possible. Don't let it creep into your controllers or service layer. You may think [YAGNI](https://en.wikipedia.org/wiki/You_aren%27t_gonna_need_it), but as your API grows, you'll definitely require this separation. ### Step 2: Set up metering as asynchronous events or logs For usage metering, prioritize asynchronous approaches that don't block your API response flow. Emit metering events to a queue or event stream that is processed and aggregated by a separate service. Don't use your current pricing model to determine which metrics to meter. Meter everything you can, even if you don't currently charge for it. This data will be invaluable when you experiment with new pricing models or need to understand customer behavior. The discipline of metering does not come naturally to many developers. To avoid the temptation to meter only what you charge for, consider a simple metering library that records all request details to a central store. We also recommend you further separate your API code from your billing system by pushing metering events to an external service. Many billing platforms ingest metering data via APIs or event streams. Our suggestion is to push metering events to your own event stream or queue, then have a separate service consume these events and push them to your billing platform. This approach decouples your API service from the billing platform, allowing you to switch billing providers without changing your core API code. Here's an example of how you might emit metering events to a Kafka topic: ```python from kafka import KafkaProducer import json producer = KafkaProducer(bootstrap_servers='localhost:9092') def emit_metering_event(customer_id, feature_id, usage): event = { "customer_id": customer_id, "feature_id": feature_id, "usage": usage } producer.send('metering-events', json.dumps(event).encode('utf-8')) ``` This code assumes you have a Kafka producer running and a topic named `metering-events`. You can then have a separate service consume these events and push them to your billing platform. ### Step 3: Centralize your pricing definitions Pricing plans should typically live in structured databases or systems designed explicitly for fast read/write and easy evolution, like a versioned NoSQL data store, a relational PostgreSQL database, or a dedicated billing platform (such as [Chargebee](https://www.chargebee.com/recurring-billing-invoicing/metered-usage-billing/), [Stripe Billing](https://docs.stripe.com/billing/subscriptions/usage-based/pricing-models), [Metronome](https://docs.metronome.com/pricing-packaging/), or [Lago](https://www.getlago.com/solutions/use-cases/hybrid-plans)). Defining your pricing plans in a structured format allows you to version them, experiment with new plans, and roll back changes if necessary. It also enables you to build tooling (like dashboards, reports, and customer-facing pricing pages) around your pricing plans. How you centralize your pricing definitions depends on your specific requirements and the tools you use. If you're using a billing platform, you might define pricing plans directly in its UI. If you're managing pricing in your database, you might create a simple CRUD interface for managing pricing plans. The most basic implementation might be a YAML file that you load into memory at startup. Whichever option you choose, ensure that your pricing definitions are **versioned** and **immutable**. Once a customer subscribes to a plan, they should remain on that plan until they explicitly change it. If, for example, the basic plan changes from $10/month to $15/month, existing customers on the basic plan should continue to pay $10/month until your system migrates them to the new plan. This migration should be a separate process that you can roll back if necessary, not a direct change to the pricing plan. Here's why centralized pricing definitions are important: - **Versioning**: Versioning your pricing plans allows you to experiment with new plans without affecting existing customers. If a new plan doesn't work out, you can roll back to the previous version without impacting your customers. - **Immutability**: Making your pricing plans immutable ensures that existing customers remain on the plan they signed up for. If you change the basic plan, existing customers should continue to pay the old price until they explicitly change plans or you migrate them to the new plan. - **Consistency**: By centralizing your pricing definitions, you ensure that all parts of your system use the same pricing data. This consistency is crucial for accurate billing and reporting. For example, you are less likely to have discrepancies between the prices shown on your website and the prices charged to customers when your pricing is centralized. ### Step 4: Create your entitlement service Your entitlement service should be a separate service that handles entitlement checks for your API. This service should be responsible for: - Checking whether a customer is entitled to use a specific feature. - Providing an API for your API service entitlement middleware to query. - Logging entitlement checks for auditing and debugging. - Integrating with your billing platform to fetch customer entitlements. The entitlement service should be a simple, stateless service that queries your pricing definitions to determine whether a customer is entitled to use a specific feature. It should not contain any business logic or pricing rules. Instead, it should be a thin layer that translates customer and feature identifiers into entitlement checks. You may be tempted to embed entitlement logic into your API service or middleware. But keeping this logic separate allows you to change billing rules, experiment with new pricing models, and integrate with different billing platforms without changing your core API code. It also makes it easier to audit and debug entitlement checks because you can log each check and its result. Teams working on your API service can focus on building features and improving performance. Teams working on your entitlement service can focus on pricing rules, billing integrations, and entitlement checks. This separation of concerns makes it easier to scale your team and your product. The entitlement middleware in your API service depends only on the clean, simple API provided by the entitlement service. This API should be well-documented and easy to use, allowing your API service to query entitlements with minimal overhead. ### Step 5: Integrate your entitlement service with your billing platform If you're using a billing platform like Chargebee, Stripe Billing, Metronome, or Lago, you'll need to integrate your entitlement service with your billing platform. Expanding on our previous stack diagram, the billing platform should form the final piece of the puzzle: ```mermaid flowchart TD client([Client]) gateway[API gateway] auth[Auth middleware] entitle[Entitlement middleware] ratelimit[Rate limiting] api[API service] meter[Metering service] entitlesvc[Entitlement service] pricing[Pricing definitions] billing[Billing platform] customer[Customer catalog] client --> gateway gateway --> auth auth --> entitle entitle -->|"Entitlement check before execution"| api api -.-> meter entitle --> entitlesvc entitlesvc --> billing billing -->|"Invoicing and subscription management"| client billing --> pricing billing --> meter billing --> customer gateway --> ratelimit subgraph monetization[Monetization layer] entitlesvc pricing meter billing customer end ``` ## Preventing billing vendor lock-in When integrating with a billing platform, it's essential to consider the risk of vendor lock-in. Just because a billing platform fits your needs now, doesn't mean it will in the future. You can prevent vendor lock-in by ensuring the following are not dependent on your billing platform: - **Pricing definitions**: Keep your pricing definitions in a structured format separate from the billing platform. If possible, use the billing platform's API to sync pricing definitions, rather than defining them directly in the platform. This way, when you can switch billing platforms by syncing your pricing definitions to the new platform instead of redefining them from scratch. - **Entitlement service**: Keep your entitlement service separate from the billing platform. The entitlement service should be responsible for checking entitlements, logging checks, and integrating with the billing platform. This separation allows you to switch billing platforms without changing your entitlement logic. - **Metering service**: Keep your metering service separate from the billing platform. The metering service should be responsible for recording usage data, emitting metering events, and integrating with the billing platform. This separation allows you to switch billing platforms without changing your metering logic. Building a separate customer catalog service that integrates with your billing platform can also help prevent vendor lock-in, but this step may be overkill for early-stage startups. ## Compliance considerations As your API usage grows, you'll encounter the following billing-related compliance considerations. ### Security compliance Billing systems often handle sensitive customer data. Make sure your billing service is PCI compliant and follows industry standards for data protection. The Payment Card Industry Data Security Standard (PCI DSS) ensures that all companies that accept, process, store, or transmit credit card information maintain a secure environment. If your billing platform handles credit card information, it must be PCI compliant. Most are, but it's worth checking. ### Tax compliance When your API business grows beyond a certain scale, you'll need to handle taxes for your customers in multiple jurisdictions. This is a surprisingly complex problem, as tax laws vary widely between countries, states, and even cities. This is also an issue that creeps up on you, then suddenly reaches a tipping point. Don't ignore it until you're forced to deal with it, but more importantly, don't build this problem into any of your own systems. Use a billing platform that handles tax compliance for you. ## Tools and libraries for getting started When implementing a flexible monetization system for your API, selecting the right tools can significantly reduce development time and complexity. Here are some tools and libraries worth considering. ### Billing platforms and subscription management These platforms handle the complex aspects of subscription billing, invoicing, and payment processing: - [Stripe Billing](https://stripe.com/billing): A comprehensive solution with flexible subscription models, usage-based billing, and extensive API capabilities. - [Polar](https://polar.sh/): A developer-first billing platform that offers a flexible subscription model, usage-based billing, and extensive API capabilities. - [Chargebee](https://www.chargebee.com/): Offers a robust subscription management platform with a focus on subscription lifecycle automation. - [Lago](https://www.getlago.com/): An open-source alternative that provides metering, subscription management, and invoicing capabilities. - [Metronome](https://metronome.com/): Specializes in usage-based pricing with metering and aggregation features. - [Paddle](https://www.paddle.com/): Handles global tax compliance and payment processing with subscription management. ### Metering libraries and services These metering tools can be used for tracking API usage: - [Prometheus](https://prometheus.io/): An open-source monitoring and alerting toolkit that can be used for metering API usage. - [OpenTelemetry](https://opentelemetry.io/): A set of APIs, libraries, agents, and instrumentation for collecting telemetry data from your services. - [Segment](https://segment.com/): A customer data platform that can be used to track API usage and customer behavior. - [CloudWatch Metrics](https://aws.amazon.com/cloudwatch/): A monitoring service for AWS resources that can be used to track API usage. - [Azure Monitor](https://azure.microsoft.com/en-us/services/monitor/): A monitoring service for Azure resources that can be used to track API usage. ### Entitlement service implementations These services can manage feature access and entitlements: - [Open Policy Agent](https://www.openpolicyagent.org/): A general-purpose policy engine that can be used for entitlement checking. - [Oso](https://www.osohq.com/): A rule-based access control engine that can be used for entitlement checking. - [Stigg](https://stigg.io/): A billing and entitlement service that can be used to manage feature access and pricing. - [LaunchDarkly](https://launchdarkly.com/blog/managing-entitlements-in-launchdarkly/): A feature-flag service that can be adapted for entitlement checking. - Custom entitlement services: Building your own service gives you maximum flexibility, especially when coupled with caching layers like Redis. API gateways can handle authentication, rate limiting, and in some cases, entitlement checking: - [Kong](https://konghq.com/): An open-source API gateway with a plugin architecture that supports custom logic for entitlement checking. - [Tyk](https://tyk.io/): An open-source API gateway with rate limiting, authentication, and access control features. - [Apigee](https://cloud.google.com/apigee): A cloud-based API management platform that can be used for entitlement checking. ## Beware the all-in-one solution Many of the tools and services listed here offer comprehensive solutions for API monetization. While using an all-in-one platform can speed up your development process, it can also lead to vendor lock-in and reduced flexibility. This doesn't mean you should avoid these platforms entirely. Instead, use them judiciously by sticking to the monetization principles in this guide. Keep your pricing definitions, entitlement checks, and metering logic separate from the billing platform. This way, you can switch billing platforms without rewriting your core API code. ## Double-beware the in-house solution Whatever you do, don't build your own billing system. When it comes to developing an in-house billing solution, you can expect complexity unlike anything you'll build in your core product. You may not be building a moon lander, but considering how complex billing systems are, you're definitely building a rocket. Billing entails dealing with the intricacies of time (some months and years are shorter than others), tax, currency conversion, payment gateways, fraud detection, and compliance. Customers expect discounts, refunds, and credits. They'll want to change plans (mid-way through a billing cycle), pause subscriptions, and cancel services. Avoid building this yourself at all costs. Use a billing platform that can handle these complexities for you. Off-the-shelf billing platforms are built by teams of experts who have spent years solving these problems. They may be expensive to the early-stage startup, but they're a bargain compared to the cost of building and maintaining your own billing system. # Paginating API responses Source: https://speakeasy.com/api-design/pagination import { Callout } from "@/mdx/components"; Pagination is a crucial concept that needs to be understood and designed into a REST API before its built. It is often forgotten about until it's too late and API consumers are already integrating with the API, so it's important to get stuck into doing things the right way early on. ## What is API Pagination? At first it's easy to imagine that collections only have a few hundred records. That not be too taxing for the server to fetch from the database, turn into JSON, and send back to the client, but as soon as the collection is getting into thousands of records things start to fall apart in wild and unexpected ways. For example, a coworking company that expected to mostly host startups of 10-50 people, but then Facebook and Amazon rock up with ~10,000 employees each, and every time somebody loads that data the entire API server crashes, along with every application that uses it, and every application that uses that. Breaking down a large dataset into smaller chunks helps to solve this, and it works much like pagination does in the browser: when searching on a functioning search engine like Duck Duck Go or Ecosia, the results are broken down into page 1, page 2... page 34295. It doesn't just throw every single result into the browser in the worlds longest slowest web response, forcing computer fans to whir until they snap out as it tries to render infinite HTML to the screen. This is pagination in action, and pagination in an API is exactly the same idea. Much like web pages it is done with query string parameters on a GET request. ``` GET /items?page=2 ``` The main difference is that the client is not seeing a list of buttons in HTML, instead they are getting metadata or links in the JSON/XML response. How exactly that looks depends on which pagination strategy is picked, and there are a few to choose from with their own pros and cons. ## Choosing a Pagination Strategy To help pick a pagination strategy, let's look at some examples and talk through the pros and cons. 1. Page-Based Pagination 2. Offset-Based Pagination 3. Cursor-Based Pagination ### Page-Based Pagination Page-based pagination uses `page` and `size` parameters to navigate through pages of data. ``` GET /items?page=2&size=10 ``` This request fetches the second page, with each page containing 10 items maximum. There are two main ways to show pagination data in the response. ```json { "data": [ ... ], "page": 2, "size": 10, "total_pages": 100 } ``` This is pretty common, but forces the client to know a whole lot about the pagination implementation, which could mean some guesswork (which could be guessed wrong), or reading a whole lot of documentation about which bit goes where and what is multiplied by whom. The best way to help the client is to give them links, which at first seems confusing but it's just [HATEOAS](https://apisyouwonthate.com/blog/rest-and-richardson-maturity-model/) (Hypermedia As The Engine Of Application State), also known as Hypermedia Controls. It's a fancy way of saying "give them links for things they can do next" and in the context of pagination that means "give them links to the next page, the previous page, the first page, and the last page." ```json { "data": [ ... ], "meta": { "page": 2, "size": 10, "total_pages": 100 }, "links": { "self": "/items?page=2&size=10", "next": "/items?page=3&size=10", "prev": "/items?page=1&size=10", "first": "/items?page=1&size=10", "last": "/items?page=100&size=10" } } ``` Whenever there is a `next` link, an API consumer can show a `next` button, or start loading the next page of data to allow for auto-scrolling. If the `next` response returns data, it will give a 200 OK response and they can show the data. If there is no data then it will still be a 200 OK but there will be an empty array, showing that everything was fine, but there is no data on that page right now. **Ease of Use** - Pro: Simple to implement and understand. - Pro: Easy for users to navigate through pages. - Pro: UI can show page numbers and know exactly how many pages there are. - Pro: Can optionally show a next/previous link to show consumers if there are more pages available. **Performance** - Con: Involves counting all records in the dataset which can be slow and hard to cache depending on how many variables are involved in the query. - Con: Becomes exponentially slower with more records. Hundreds are fine. Thousands are rough. Millions are horrendous. **Consistency** - Con: When a consumer loads the latest 10 records, then a new record is added to the database, then a user loads the second page, they'll see one of those records twice. This is because there is no such concept as a "page" in the database, just saying "grab me 10, now the next 10" does not differentiate which records they actually were. ### Offset-Based Pagination Offset-based pagination is a more straightforward approach. It uses `offset` and `limit` parameters to control the number of items returned and the starting point of the data, which avoids the concept of counting everything and dividing by the limit, and just focuses on using offsets to grab another chunk of data. ``` GET /items?offset=10&limit=10 ``` This request fetches the second page of items, assuming each page contains a maximum of 10 items, and does not worry itself with how many pages there are. This can help with infinite scrolls or automatically "importing" lots of data one chunk at a time. There are two main ways to show pagination data in the response. ```json { "data": [ ... ], "meta": { "total": 1000, "limit": 10, "offset": 10 } } ``` Or with hypermedia controls in the JSON: ```json { "data": [ ... ], "meta": { "total": 1000, "limit": 10, "offset": 10 }, "links": { "self": "/items?offset=10&limit=10", "next": "/items?offset=20&limit=10", "prev": "/items?offset=0&limit=10", "first": "/items?offset=0&limit=10", "last": "/items?offset=990&limit=10" } } ``` **Ease of Use** - Pro: Simple to implement and understand. - Pro: Easily integrates with SQL `LIMIT` and `OFFSET` clauses. - Pro: Like page-based pagination this approach can also show next/previous buttons dynamically when it's clear there are more records available. - Con: Does not help the UI build a list of pages if they want to show "Page 1, 2, ... 20." They can awkwardly do maths on the total / limit but it's a bit weird. **Performance** - Con: Can become inefficient with large datasets due to the need to scan through all previous records. - Con: Performance degradation is significant as the offset increases. **Consistency** - Con: The same problems exist for offset pagination as page pagination, if more data has been added between the first request and second being made, the same record could show up in both pages. **See this in action** - [YouTube Data API](https://developers.google.com/youtube/v3/guides/implementation/pagination) - [Reddit API](https://www.reddit.com/dev/api/) ### Cursor-Based Pagination Cursor-based pagination uses an opaque string (often a unique identifier) to mark the starting point for the next subsection of resources in the collection. It's often more efficient and reliable for large datasets. ``` GET /items?cursor=abc123&limit=10 ``` Here, `abc123` represents the last item's unique identifier from the previous page, this could be a UUID, but it can be more dynamic than that. APIs like Slack will base64 encode information with a field name and a value, even adding sorting logic, all wrapped up in an opaque string. For example, `dXNlcjpXMDdRQ1JQQTQ=` would represent `user:W07QCRPA4`. Obfuscating the information like this aims to stop API consumers hard-coding values for the pagination, which allows for the API to change pagination logic over time without breaking integrations. The consumers can simply pass the cursor around to do the job, without worrying about what it actually involves. It can look a bit like this: ```json { "data": [...], "next_cursor": "xyz789", "limit": 10 } ``` To save the client even having to think about cursors (or knowing the name of the query parameters for cursor or limit), links can once again save the day: ```json { "data": [ ... ], "links": { "self": "/items?cursor=abc123&limit=10", "next": "/items?cursor=xyz789&limit=10", "prev": "/items?cursor=prevCursor&limit=10", "first": "/items?cursor=firstCursor&limit=10", "last": "/items?cursor=lastCursor&limit=10" } } ``` **Ease of Use** - Pro: API consumers don't have to think about anything and the API can change the cursor logic. - Con: Slightly more complex to implement than offset-based pagination. - Con: API does not know if there are more records available after the last one in the dataset so has to show a next/previous link which may return no data. (You can grab limit+1 number of records to see if it's there, but that's a bit of a hack which could end up being slower. Benchmarks are your friend.) **Performance** - Pro: Generally more efficient than offset-based pagination depending on the data source being used. - Pro: Avoids the need to count records to perform any sort of maths which means larger data sets can be paginated without suffering exponential slowdown. **Consistency** - Pro: Cursor-based pagination data remains consistent in more scenarios, even if new data is added or removed, because the cursor acts as a stable merker identifying a specific record in the dataset instead of "the 10th one" which might change between requests. **See it in action** - [Twitter API](https://developer.twitter.com/en/docs/twitter-api) - [Instagram Graph API](https://developers.facebook.com/docs/instagram-api/) - [Slack API](https://slack.engineering/evolving-api-pagination-at-slack/) ### Choosing a strategy Choosing the right pagination strategy depends on the specific use case and dataset size. Offset-based pagination is simple but may suffer from performance issues with large datasets. Cursor-based pagination offers better performance and consistency for large datasets but come with added complexity. Page-based pagination is user-friendly but shares similar performance concerns with offset-based pagination. Using links instead of putting metadata in the response allows for more flexibility over time with little-to-no impact on clients. ## Where Should Pagination Go? In all of these examples there's been the choice between sending some metadata back for the client to construct their own pagination controls, or sending them links in JSON to avoid the faff. Using links is probably the best approach, but they don't have to go in the JSON response. Instead use the more modern approach: [RFC 8299: Web Linking](https://www.rfc-editor.org/rfc/rfc8288). ```http Link: ; rel="first", ; rel="next", ; rel="last" ``` Popping them into HTTP headers seems like the cleaner choice instead of littering responses with metadata. It's also a slight performance putting this into headers because HTTP/2 adds [header compression via HPAK](https://blog.cloudflare.com/hpack-the-silent-killer-feature-of-http-2/). As this is a common standard instead of a convention, [generic HTTP clients like Ketting](https://apisyouwonthate.com/blog/ketting-v5-hypermedia-controls/) can pick this information up to provide a more seamless client experience. Either way, pick the right pagination strategy for the data source, document it well with a dedicated guide in API documentation, and make sure it scales up with a realistic dataset instead of testing with a handful of records as assuming it scales Adding or drastically changing pagination later could be a whole mess of backwards compatibility breaks. Pagination can be tricky to work with for API clients, but Speakeasy SDKs can help out. Learn about adding pagination{" "} to your Speakeasy SDK. # Picking an API architecture Source: https://speakeasy.com/api-design/picking-architectures Planning to build an API? Choosing the right API architecture can feel like a gamble. You know that REST is reliable, GraphQL is gaining traction, gRPC is designed for high performance, WebSockets and SSE shine in real time, and SOAP... well, it's a legacy standard, but it's still out there. It's a crowded ecosystem and the stakes are high: Choosing the wrong API architecture can lead to high technical debt, slower development, scalability issues, and, even worse, failure to meet long-term business goals. This article cuts through the noise by focusing on a few key evaluation criteria – scalability, team expertise, integration needs, and maintainability – so that you can make a choice that meets immediate requirements while future-proofing your system. ## Matching API architectures to project requirements The starting point for choosing an API architecture isn't the tools; it's understanding your project's requirements. You're building a system to expose and manage data, so you need to know from the outset what your users need, how your data flows, and what kind of interactions will occur. Answering the following questions will help you define your project's requirements and use case, guiding you to the right architecture for your needs. * Are you building a simple client-server communication system? * Does the project require real-time updates, or can it work with periodic data retrieval? * Is your data model simple and predictable, or does it involve complex, nested structures? * Are you dealing with sensitive information that demands strict security measures? * Is this a lightweight project, or are you building a large-scale, scalable system? Now that you've considered your project's requirements, let's take a closer look at the available architecture options. ## REST: A proven architecture for client-server communication Say you're building an e-commerce platform. The application is straightforward at the start: Users can browse products, add items to their carts, and place orders. However, as the business plans to scale rapidly, the API must efficiently handle increased traffic and streamline developer onboarding. This is where [REST (**RE**presentational **S**tate **T**ransfer)](https://web.archive.org/web/20250908222822/http://restfulapi.net/) excels. REST uses a stateless, resource-based design, where endpoints represent entities like products or orders. Each endpoint corresponds to resources that can be manipulated using standard HTTP methods (GET, POST, PUT, and DELETE), making REST an intuitive choice for developers. In the case of your e-commerce application, endpoints could be designed like this: * GET `/products` to fetch a list of products. * POST `/orders` to place a new order. Additionally, you can add endpoints to handle user authentication with `OAuth` or `JWT`. Beyond authentication, REST's resource-oriented design provides an intuitive and scalable foundation for extending your API. Whether you're integrating real-time analytics, enhancing inventory tracking, or enabling multi-channel third-party interactions, REST offers a modular and predictable approach that fosters interoperability and future growth. For example, you can use the Flask framework in Python to build a RESTful API. Here is how you can define a simple endpoint to fetch a list of products: ```python filename="app.py" from flask import Flask, jsonify, request app = Flask(__name__) @app.route('/products', methods=['GET']) def get_products(): products = [ {'id': 1, 'name': 'Product 1'}, {'id': 2, 'name': 'Product 2'}, {'id': 3, 'name': 'Product 3'}, ] return jsonify(products) if __name__ == '__main__': app.run(debug=True) ``` The code above will expose a GET endpoint at the `/products` path, returning a `JSON` response with a list of products. This simplicity is one of REST's key strengths – developers can quickly prototype and deploy APIs with minimal setup, using familiar web technologies and tools. Other frameworks, such as FastAPI and Django, have many tools and built-in features following REST principles that can help you implement authentication, authorization, and more. ### Scaling with REST: Better horizontally Depending on the type of framework you are using, scaling horizontally (adding more servers) is straightforward because REST is stateless, meaning that each request from a client contains all the information necessary for the server to process it. Because the server doesn't need to retain the session state between requests, distributing requests across multiple servers is straightforward, allowing new servers to be added without affecting the existing ones. This facilitates load balancing, as REST's stateless nature allows load balancers to distribute the load evenly across multiple servers. In the e-commerce application example, with 1,000 users accessing 500 products, the high-level view of the backend architecture would be as follows: ![Architecture of the e-commerce application](/assets/api-design/architecture-ecommerce.png) Here's how the backend processes requests: - A client makes a request. - The request first goes to the load balancer, which then distributes the request to one of the servers. - The server receives the request and processes it. - The server sends a response back to the client. This architecture can be easily replicated and expanded to support large-scale operations, enabling it to handle thousands or even millions of users simultaneously – something [Instagram](https://highscalability.com/designing-instagram/) has successfully achieved. ## GraphQL: An architecture to query exactly what you need WebSocket and SSE are great for real-time communication, but what happens if you need an API that can handle some REST-like operations while allowing you to build a real-time system without too much hassle? Let's discuss GraphQL. Facebook launched in 2004 and quickly scaled to a multibillion-dollar company with over a billion users by 2012. During this growth, the Facebook team encountered significant challenges with their REST-based APIs. Some of the shortcomings they discovered with REST were: - **Slow on network:** The iOS app had to make multiple API calls to fetch all the required data, causing delays. - **Fragile integration:** Any API changes risked breaking the app, as updates weren't always reflected in the client, leading to crashes. - **Tedious process:** Developers had to manually update client-side code and models whenever the API responses changed, making maintenance time-consuming. To overcome the limitations of REST, the Facebook team built a new API architecture called [GraphQL](https://graphql.org/). GraphQL is a query language for APIs that allows clients to request exactly the data they need and the server to respond with only that data. For example, let's say you are fetching data from an e-commerce API to retrieve a list of products. Here's what the request would look like in a REST API: ```http GET /products // response [ { "id": 1, "name": "Product 1", "price": 10.99, "description": "This is a product description", "createdAt": "2023-01-01T00:00:00.000Z", "updatedAt": "2023-01-01T00:00:00.000Z" } ] ``` In a GraphQL API, the request would look like this: ```graphql query { products { id name price } } ``` And the response would look like this: ```json { "data": { "products": [ { "id": 1, "name": "Product 1", "price": 10.99 } ] } } ``` The benefits of GraphQL include: - **Optimized network usage:** GraphQL fetches only the required data in a single request, supports real-time updates via WebSockets, and uses a single endpoint for all queries and mutations, simplifying API management. - **Robust static typing:** Reduces runtime errors, empowers clients to control response formats, and supports gradual backward compatibility without versioning, making server-side maintenance easier. - **Enhanced developer productivity:** GraphQL offers tools like code generators and API explorers, automatically generates up-to-date documentation, and can easily integrate with existing REST APIs or backend systems. ### Scaling with GraphQL GraphQL is an excellent choice for scaling horizontally, as it allows you to build a single endpoint for all requests. It's also easy to migrate to a GraphQL API, as it's a query language, so you can use it with any programming language or framework. [Uber ditched its old WebSocket-based live-chat architecture for GraphQL](https://blog.bytebytego.com/p/how-uber-built-real-time-chat-to) to take advantage of this scalability and flexibility. While WebSocket APIs are great for real-time communication, the performance cost is high at scale. As the WebSocket protocol is better suited to vertical scaling, it isn't ideal for building a region-wide API. Additionally, the Uber team had challenges with debugging, testing, and reliability in the old system, and found implementing a retry mechanism to be overly complex. GraphQL addressed these issues and was easy to adopt, as it was already integrated into other Uber services. Uber observed significant improvements after implementing the new architecture, including: - Each machine supported 10,000 socket connections, with horizontal scaling enabling 20 times more events than the previous architecture. - Existing traffic was routed through the new system with the old agent interface to test reliability, maintaining latency within defined SLAs. - Offline agents were automatically logged out based on missed acknowledgments, resolving issues from the previous system where inactive agents increased customer wait times. - The chat channel now handles 36% of Uber's contact volume, with plans to increase its share in the coming months. - The contract delivery error rate dropped significantly, from 46% to 0.45%. - The new architecture reduced complexity with fewer services and protocols, and better observability. With GraphQL, you can design an API architecture that evolves from a simple API to a real-time API, while scaling both horizontally and vertically. However, a common complaint about GraphQL is its lack of integration with OpenAPI, which makes sense as it's a non-REST API. ## gRPC: A new standard for high-performance APIs [gRPC (Google Remote Procedure Call)](https://grpc.io/) is a recent development in API design that promises a better API architecture by combining the scalability and simplicity of REST with the real-time capabilities of WebSockets while addressing some of the complexity and over-fetching challenges often associated with GraphQL. In 2015, Google open-sourced gRPC, a modern, high-performance, open-source framework designed for efficient communication between microservices. While REST and GraphQL are widely used for APIs, gRPC was developed to address the limitations of traditional API architectures, especially in distributed systems and high-performance use cases. gRPC is built on Protocol Buffers (Protobuf), a lightweight and highly efficient binary serialization format, which replaces the verbose JSON and XML payloads of REST and SOAP. It uses HTTP/2, which supports multiplexing, bidirectional streaming, and header compression. In a REST API, retrieving a product would look like this: ```http GET /products/1 { "id": 1, "name": "Product 1", "price": 10.99, "description": "This is a product description", "createdAt": "2023-01-01T00:00:00.000Z", "updatedAt": "2023-01-01T00:00:00.000Z" } ``` In a gRPC API, the request would look like this: ```protobuf syntax = "proto3"; service ProductService { rpc GetProduct (ProductRequest) returns (ProductResponse); } message ProductRequest { int32 id = 1; } message ProductResponse { int32 id = 1; string name = 2; float price = 3; string description = 4; } ``` To fetch a product: - The client makes a GetProduct RPC call to the server, sending a binary-encoded request. - The server responds with the requested data in a compact binary format. This approach ensures smaller payloads, faster communication, and lower resource usage than REST or GraphQL. So, what makes gRPC a better choice for API architectures? - **High performance:** Protobuf serialization and HTTP/2 make gRPC highly efficient regarding network bandwidth and processing speed, especially in low-latency environments. - **Bidirectional streaming:** Unlike REST, gRPC natively supports real-time bidirectional communication using streams, which is ideal for video calls, live dashboards, or IoT applications. - **Strong typing:** Protobuf provides a strongly typed schema, ensuring consistency between client and server implementations. - **Interoperability:** gRPC supports multiple programming languages, making it an excellent choice for polyglot systems. ### Scaling with gRPC Despite being relatively new, gRPC has been widely adopted by leading tech companies like Google, Uber, and Netflix. In 2019, Uber [moved its RAMEN tool to gRPC](https://www.uber.com/en-NG/blog/ubers-next-gen-push-platform-on-grpc/), a next-generation protocol. This upgrade improved the RAMEN tool's speed and performance, delivering the following benefits: - Instant acknowledgments that improve reliability, enable real-time RTT measurement, and help distinguish between message and network losses, with a slight increase in data transfer. - Abstraction layers that support stream multiplexing, network prioritization, and flow control, optimizing data usage and reducing latency. - Diverse payload serializations and robust client implementations across languages that enable easy support for various apps and devices. ## WebSockets: An architecture for real-time communication systems REST APIs are great for applications involving CRUD operations and structured request-response interactions where there is no need for real-time updates, and they scale reasonably well. But what happens when you need to handle real-time updates? One solution is polling, a technique that allows the client to request the server periodically to check for updates. However, even if polling does the job, it can be inefficient and lead to many unnecessary requests, especially if the server is under heavy load, and it can flood the database. Consider a real-time, high-frequency trading application – just one example where API design must strike a balance between speed and reliability. Users of this trading application need to see price changes as soon as they occur, but they also need to be able to send buying and selling orders to the server. While REST might seem like a viable option for building this app, you would likely face the following challenges: - Price changes can happen within milliseconds or even microseconds, with hundreds of updates taking place every second. - Users need to send orders to the server and make decisions based on real-time price changes. Imagine thousands of users simultaneously reading price changes and sending orders to the server. With REST, the only way to receive data is to poll the server as frequently as possible. As HTTP requests include headers, other properties, and sometimes bodies, frequent polling can overwhelm server bandwidth. Thousands of users accessing a high-demand resource at once creates bottlenecks – a problem that real-time APIs are designed to prevent. A [WebSocket API](https://developer.mozilla.org/en-US/docs/Web/API/WebSockets_API) enables full-duplex communication, maintaining an open connection between the client and server so messages can flow in both directions instantly. Here's how WebSockets works: - The client sends a WebSocket handshake request to the server. - If accepted, the server upgrades the connection from HTTP to WebSocket. - The client and server can send messages over the persistent connection. The advantages of WebSockets include: - Full-duplex communication, so the client and server can send messages in both directions. - Low latency, as messages are sent over a persistent connection. - Support for different types of messages, such as text, binary, and even JSON. - Support for authentication and authorization so the server can control who can access the connection. This is what a typical WebSocket architecture looks like. ![Architecture of a WebSocket](/assets/api-design/websocket-communication.png) WebSockets is a great choice for our high-frequency trading application. Let's see how this could be implemented in a Node.js application, using channels to allow users to send orders and receive price changes in real time. ```javascript filename="app.js" import {WebSocketServer} from 'ws'; import Redis from 'ioredis'; const PORT = 8080; // Redis configuration const redisPublisher = new Redis(); // For publishing messages (e.g., orders) const redisSubscriber = new Redis(); // For subscribing to price updates // Create WebSocket server const wss = new WebSocketServer({port: PORT}); console.log(`WebSocket server is running on ws://localhost:${PORT}`); // Keep track of connected clients const clients = new Set(); // Handle client connections wss.on('connection', (ws) => { console.log('New client connected'); clients.add(ws); // Handle incoming messages from clients ws.on('message', async (message) => { try { const parsedMessage = JSON.parse(message); switch (parsedMessage.type) { case 'PLACE_ORDER': await handlePlaceOrder(parsedMessage.payload, ws); break; case 'SUBSCRIBE_PRICES': handleSubscribePrices(ws); break; default: ws.send(JSON.stringify({error: 'Unknown message type'})); } } catch (err) { console.error('Error handling message:', err); ws.send(JSON.stringify({error: 'Invalid message format or internal error'})); } }); // Handle client disconnections ws.on('close', () => { console.log('Client disconnected'); clients.delete(ws); }); }); // Broadcast a message to all connected clients const broadcast = (data) => { clients.forEach((client) => { if (client.readyState === client.OPEN) { client.send(JSON.stringify(data)); } }); }; // Handle "PLACE_ORDER" messages const handlePlaceOrder = async (order, ws) => { try { const orderData = {...order, timestamp: new Date().toISOString()}; await redisPublisher.publish('orders', JSON.stringify(orderData)); // Publish the order to Redis console.log('Order placed:', orderData); // Respond to the client ws.send(JSON.stringify({type: 'ORDER_PLACED', payload: {status: 'SUCCESS', orderId: Date.now()}})); } catch (error) { console.error('Error placing order:', error); ws.send(JSON.stringify({error: 'Failed to place order'})); } }; // Handle "SUBSCRIBE_PRICES" messages const handleSubscribePrices = (ws) => { console.log('Client subscribed to price updates'); // Subscribe to the "prices" channel in Redis redisSubscriber.subscribe('prices'); redisSubscriber.on('message', (channel, message) => { if (channel === 'prices' && ws.readyState === ws.OPEN) { ws.send(JSON.stringify({type: 'PRICE_UPDATE', payload: JSON.parse(message)})); } }); // Clean up when the client disconnects ws.on('close', () => { console.log('Client unsubscribed from price updates'); redisSubscriber.unsubscribe('prices'); }); }; ``` The code above creates a WebSocket server that listens on port 8080. When a client connects, the server handles the incoming messages and broadcasts them to all connected clients. Redis is used here to facilitate real-time publish-subscribe messaging by distributing price updates to WebSocket clients and handling client orders for asynchronous backend processing. ### WebSocket APIs scale better vertically WebSockets are generally better suited for vertical scaling (by increasing a server's resources) than horizontal scaling. Here's why: - **Persistent connections:** WebSockets maintain bidirectional, long-lived connections between client and server, requiring each connection to stay open and active on the server. This makes it difficult to distribute workloads across multiple servers. - **Stateful management:** Each WebSocket connection holds a state on the server, including user sessions and subscribed topics, which complicates horizontal scaling without a shared state. Vertically scaling WebSocket APIs is a more suitable option, as a powerful server with sufficient CPU and memory can efficiently handle tens of thousands of WebSocket connections without inter-server communication overhead. ## Server-sent events: A unidirectional alternative to WebSockets If you don't need users to send data to the server, you can opt to use [server-sent events (SSE)](https://developer.mozilla.org/en-US/docs/Web/API/Server-sent_events/Using_server-sent_events) instead of WebSockets. Unlike WebSockets' full-duplex communication, SSE is unidirectional, sending events only from the server to the client. This is the technology behind RAMEN (**R**ealtime **A**synchronous **ME**ssaging **N**etwork), [Uber's real-time push messaging platform](https://www.uber.com/en-NG/blog/real-time-push-platform/). Uber uses RAMEN, built on SSE, to maintain persistent connections with millions of devices and deliver prioritized, real-time updates, such as trip statuses and driver locations. This platform allows Uber to handle over 1.5 million concurrent streaming connections and push 250,000 messages per second across multiple applications. ## SOAP: A legacy standard for APIs Some tools are so deeply ingrained in an industry that they defeat the forces of competition and time. Let's discuss SOAP. Introduced in 1999, [SOAP (Simple Object Access Protocol)](https://www.microfocus.com/documentation/silk-performer/205/en/silkperformer-205-webhelp-en/GUID-FEFE9379-8382-48C7-984D-55D98D6BFD37.html) is one of the earliest protocols designed for web services. It established a standard for exchanging structured information in distributed systems, particularly in enterprise environments requiring robust security, transactionality, and reliability. While SOAP has mainly been replaced by REST, GraphQL, and gRPC in modern architectures, it continues to thrive in the banking, insurance, and healthcare industries. SOAP operates over XML, making it verbose but highly structured, and it's transport-agnostic, meaning it can run over protocols like HTTP, SMTP, or message queues. This flexibility and reliability have kept SOAP relevant when strict compliance, security, and transactional integrity are critical, which is why this architecture continues to power many of the world's largest banks, insurance companies, and healthcare providers. SOAP works by defining an envelope for a message that includes: - A header containing metadata for processing, such as authentication or transaction management. - A body containing the actual data payload, typically encoded in XML. Here's an example of a SOAP request to fetch a product: ```xml 1 ``` And here is the response: ```xml 1 Product 1 10.99 This is a product description ``` Although SOAP requests may seem verbose compared to REST or gRPC, the protocol compensates with its focus on reliability and security. SOAP natively supports WS-Security, which includes encryption, digital signatures, and token-based authentication. Other benefits of SOAP include: - SOAP supports ACID transactions through [WS-AtomicTransaction](https://learn.microsoft.com/en-us/dotnet/framework/wcf/feature-details/using-ws-atomictransaction), ensuring operations like money transfers are executed reliably without partial failures. - Unlike REST and GraphQL, which rely on HTTP, SOAP can operate over multiple transport protocols, including SMTP and JMS, enabling broader integration options. - SOAP includes WS-ReliableMessaging, which guarantees message delivery even in unreliable networks. This is critical for scenarios like inter-bank communications or cross-border payments. ### Scaling with SOAP Scaling SOAP-based systems presents unique challenges due to their stateful nature, verbose XML payloads, and reliance on complex features like transactional messaging and WS-Security. Choosing SOAP with the expectation of high performance may be unrealistic. However, scaling is not necessarily about how fast a system can process but also about how solid the system can be. For example, SOAP handles long-running operations efficiently with asynchronous messaging over protocols like SMTP and XMPP, ensuring reliability and retries without overloading the system. While REST, GraphQL, and gRPC have largely replaced SOAP, it is a legacy technology valued for its reliability, security, and interoperability. However, SOAP's performance and scalability limitations make it less suitable for modern architectures. ## Making pragmatic choices Choosing the right API architecture for your needs doesn't have to be complicated. REST remains the safest choice for most applications due to its simplicity, predictability, and extensive industry adoption. Scalability challenges are well-documented, with countless resources offering proven solutions. The mature REST ecosystem provides powerful tools like Swagger, OpenAPI, and Speakeasy, a tool that automatically generates SDKs from an OpenAPI specification. If real-time interactions are a priority, WebSockets and SSE offer efficient solutions to complement REST. Other standards are valuable but often better suited to specific use cases. To simplify the decision-making process, we've created a flowchart-style framework to help you identify the most suitable architecture based on your project's requirements. This is complemented by a decision table that maps everyday use cases to recommended architectures with concise justifications. ### Decision flowchart ![Flowchart outlining the decision-making process for choosing the most suitable API architecture based on project requirements](/assets/api-design/api-architecture-decision.png) ### Decision table | Scenario | Recommended architecture | Why? | |----------------------------------------------|------------------------------|-------------------------------------------------------------------------------------------| | Building a basic CRUD API | REST | Simple, widely supported, and intuitive for basic client-server communication. | | Large-scale, real-time collaboration | WebSockets | Supports bidirectional, low-latency communication, ideal for applications like Slack. | | Real-time updates, unidirectional | SSE | Lightweight and easy for server-to-client real-time updates. | | Highly flexible data retrieval | GraphQL | Allows clients to fetch only the needed data, reducing over- and under-fetching. | | Performance-critical microservices | gRPC | Binary serialization with Protobuf ensures low latency and high throughput. | | Enterprise-grade financial system | SOAP | Provides transactional integrity, built-in security, and reliable messaging. | | Multi-client application with evolving needs | GraphQL | Adaptable to various client needs without requiring API versioning. | ### How to use the framework 1. Follow the flowchart to narrow your options based on project requirements. 2. Use the decision table to confirm the best fit for your specific scenario. By understanding the trade-offs and aligning your choice with your application's goals, you can make an informed, pragmatic decision. # rate-limiting Source: https://speakeasy.com/api-design/rate-limiting Rate limiting is the art of trying to protect the API by telling "overactive" API consumers to calm down a bit, telling clients to reduce the frequency of their requests, or take a break entirely and come back later to avoid overwhelming the API. ## Why bother with rate limiting The main reason for rate limiting is to keep an API running smoothly and fairly. If all clients could fire off requests as fast as they like, it's only a matter of time before something breaks. A spike in traffic (whether accidental or malicious) can overwhelm servers, leading to slowdowns, crashes, or unexpected high infrastructure costs. Rate limiting is also about fairness. If there are loads of users accessing an API, it's important to make sure one consumers mistakes do not affect another. For public APIs, it's about making sure no one user can hog all the resources. For businesses, this could be different limits for free and various paid tiers to make sure profit margins are maintained. ## How does API rate limiting work? How can an API know when a client is making too many requests? That's where rate limiting comes in. Rate limiting is a system that tracks the number of requests made by a particular target (based on IP address, API key, user ID, or other headers), within a defined time window. The way this is implemented can vary, but the general process is the same: - *Request Received* - A client makes a request to the API, asking for some data or to perform an action. - *Identify the client* - The system identifies the client making the request, usually by looking at the IP address, API key, or other identifying information. - *Check usage history* - The system checks how many requests the client has made in the current time window, and compares it to the limit. - *Allow or deny the request* - If the client has made too many requests, the system denies the request with a `429 Too Many Requests` [status code](/api-design/status-codes). If the client is within the limit, the request is processed as normal. ## Different rate limiting strategies There are a few different strategies for rate limiting, each with its own advantages and disadvantages. ![](/assets/api-design/rate-limiting-strategies.gif) - **Token bucket:** the system has a bucket of tokens, and each request consumes a token. Tokens are added to the bucket at regular intervals, 100 tokens a minute, or 1,000 tokens per hour. If there are no tokens left, the request is denied. Clients are rewarded for taking time out and accrue more tokens as they do. This can lead to a lot of sudden bursts of activity, but should generally keep an average amount of traffic going through the system. - **Fixed window:** the system sets a fixed limit for a specific time window. For example, "Make 100 requests per minute." This is the most common approach, but it can lead to very "lumpy" API traffic, where many clients are making the maximum number of requests at the start of a minute. This means an API can be stressed at the start of each minute and bored for the rest of it. - **Sliding log:** instead of using the same time windows for all clients, the system sets a maximum number of requests for any 60 second period. This avoids the lumpy traffic concerns of many clients all maxing out at the start of the window, then doing nothing for the rest of it, as they would all have their own windows starting and stopping at different times depending on their usage. - **Sliding window:** is a dynamic approach, adjusting limits based on real-time traffic patterns to optimize system performance and ensure fair access for all. This can be more complex to implement, but it can lead to a more efficient use of resources and a better experience for API consumers. ## Different limit targets There are a lot of choices to be made when it comes to rate limiting, and the first is: who or what are we trying to limit? Here are a few common targets for rate limiting: - **User-specific rate limits:** Identifying a user by their API key or user ID and setting a rate limit for that user. This is useful for ensuring that no single user can overwhelm the API and slow it down for others. - **Application-specific rate limits:** Identifying an application by its API key and setting a rate limit for that application. This is useful for ensuring that a misconfigured application cannot affect stability for other applications. - **Regional rate limits:** Manage traffic from different geographic regions, to make sure an API can continue to service critical regions, whilst still allowing other regions to access the API. ### Implementing rate limiting in HTTP Rate limiting can be implemented at various levels, from the network layer to the application layer. For HTTP APIs, the most common approach is to implement rate limiting at the application layer with HTTP "middlewares" that keep track of these things, or API gateways which handle rate limiting like Zuplo, Kong, Tyk, etc. Wherever the rate limiting is implemented, there are a few standards that can be leveraged to avoid reinventing the wheel. The first is to return a HTTP error with a [status code](/api-design/status-codes) of `429 Too Many Requests` (as defined in [RFC 6585](https://www.rfc-editor.org/rfc/rfc6585.html)). This tells the client that they've exceeded the rate limit and should back off for a while. ```http HTTP/2 429 Too Many Requests ``` Instead of leaving the client to guess when they should try again (likely leading to lots of poking and prodding adding more traffic to the API), the `Retry-After` header can be added to a response with a number of seconds, or a specific time and date of when the next request should be made. ```http HTTP/2 429 Too Many Requests Retry-After: 3600 ``` Why not also add some [proper error response](/api-design/errors) to explain why the request was rejected, for any API consumer developers not familiar with these concepts. ```http HTTP/2 429 Too Many Requests Content-Type: application/json Retry-After: 3600 { "error": { "message": "Rate limit exceeded", "code": "rate_limit_exceeded", "details": "You have exceeded the rate limit for this API. Please try again in 1 hour." } } ``` Doing all of this makes it clear to the client that they have entered a rate limit, and give them the information they need to know when they can try again, but there is more that can be done to make this more user friendly. ### Rate limit headers Documenting the rate limit in the response headers can help API consumers to understand what's going on. There are various conventions for headers to help consumers understand more about what the rate limiting policy is, how much of the limit has been used, and what is remaining. GitHub for example uses the `X-RateLimit-Limit`, `X-RateLimit-Remaining`, and `X-RateLimit-Reset`. Twitter uses `X-Rate-Limit-Limit`, `X-Rate-Limit-Remaining`, and `X-Rate-Limit-Reset`. Similar but different, which causes all sorts of confusion. Designing an API to be the most user friendly means relying on standards instead of conventions, so it's worth looking at the [RateLimit header draft RFC](https://datatracker.ietf.org/doc/draft-ietf-httpapi-ratelimit-headers/) which outlines one new `RateLimit` header to cover all those use cases and a few more. The following example shows a `RateLimit` header with a policy named "default", which has another 50 requests allowed in the next 30 seconds. ``` RateLimit: "default";r=50;t=30 ``` The `RateLimit` header focuses on the current state of the various quotes, but it doesn't provide information about the policy itself. The same draft RFC also outlines a `RateLimit-Policy` header which can be used to provide information about how the policy works. This example shows two policies, "default" and "daily". The "default" policy has a quota of 100 requests and a window of 30 seconds, while the "daily" policy has a quota of 1000 requests and a window of 86400 seconds (24 hours). ```http RateLimit-Policy: "default";q=100;w=30,"daily";q=1000;w=86400 ``` Combining these two headers can provide a lot of information to API consumers to know what the rate limits are, how much they have used, and when they can make more requests. This can be a bit of work to set up, but it allows API consumers to interact with an API more effectively, with less frustration, and keep everything running smoothly. ### Alternatives to Rate Limiting Some people argue that rate limiting is a blunt tool. It can be frustrating for users who hit the limit when they're trying to get work done. Poorly configured rate limiting can be fairly arbitrary. Consider an API that could theoretically handle 1000 requests per second. If there are 1000 users, each with a rate limit of 1 request per second, the API would be maxed out. If that same API with 1000 users and only two of them are using up the their maximum quotas, then the API could absolutely handle the load, and the API is sitting their underutilized sitting around waiting for potential activity which wont come. Not only is that a waste of server resources (hardware, electricity, CO2 emissions), but it's also frustrating for those users who are constantly being told to calm down when they could be using the API to handle more activity; activity which could be profitable. One alternative approach is known as **backpressure**. This is a more dynamic system which tells clients to ease up when the system is under strain, with a `503 Service Unavailable` response with a `Retry-After` header. This could be applied to the entire API, to specific users, or even specific endpoints that are more resource intensive. Quota-based systems are another alternative. Instead of measuring requests per second or minute, users are assigned a monthly allowance. This works well for subscription-based APIs, where users pay for a certain amount of access. If they make a mistake and use up their quota too quickly, they can buy more, and other API consumers can still access the API. This lends itself better to auto-scaling up (and back down) based on number of active users and usage. ### Final Thoughts Rate limiting begins as a technical safeguard for an API (which makes managing it easier) but ensures nobody is hogging resources (which keeps users happily using the product). It's worth thinking about where and how to implement it, how to communicate it, and how to make it as user-friendly as possible. It's not always simple for junior developers to figure out how to work with rate limiting and they might not know all the HTTP status codes and headers. The more tooling you can provide to assist your users with responding to your rate limiting, the better. # Sending request data Source: https://speakeasy.com/api-design/request-body import { CodeWithTabs } from "@/mdx/components"; Understanding how to properly structure and handle request data is crucial for building robust APIs. This guide covers best practices and common patterns for working with API request data. ## URL structure Every API request starts with a URL that identifies the resource and any query parameters: ```http GET /api/v1/places?lat=40.759211&lon=-73.984638 HTTP/1.1 Host: api.example.org ``` Key components: - Resource path: Identifies what you're interacting with. - Query parameters: Filter, sort, or modify the request. - API version: Often included in the path. ## HTTP request bodies Requests can also have a "request body", which is a payload of data being sent the API for processing. It is very frowned upon to use a request body for the HTTP method `GET`, but expected for `POST`, `PUT`, `PATCH`, and `QUERY`. The request body can be in a variety of formats, but the most common are JSON, XML, and form data. ```http POST /places HTTP/1.1 Host: api.example.org Content-Type: application/json { "name": "High Wood", "lat": 50.464569783, "lon": -4.486597585 } ``` This `POST` request to the `/places` endpoint is trying to add a new place to the API, once again using a JSON body. The `Content-Type` header lets the server know to expect JSON data, so it can parse the body and create a new place with the name "High Wood" and the coordinates `50.464569783, -4.486597585`. So far the examples of HTTP requests and responses have been using text, but in reality they are just a series of bytes. The text is just a human-readable representation of the bytes, and the tools that interact with the API will convert the text to bytes before sending it, and convert the bytes back to text when receiving it. Most of you will interact with APIs using a programming language, and the code to send a request will look something like this: 'application/json', ]; $payload = [ 'name' => 'High Wood', 'lat' => 50.464569783, 'lon' => -4.486597585, ]; $req = $client->post('/places', [ 'headers' => $headers, 'json' => $payload, ]);`, }, { label: "main.rb", language: "ruby", code: `conn = Faraday.new( url: 'https://api.example.org', headers: { 'Content-Type' => 'application/json' } ) response = conn.post('/places') do |req| req.body = { name: 'High Wood', lat: 50.464569783, lon: -4.486597585, }.to_json end`, } ]} /> HTTP tooling is essentially the same thing no matter the language. It's all about URLs, methods, body, and headers. This makes REST API design a lot easier, as you have a "uniform interface" to work with, whether everything is following all these set conventions already. Requests like `POST`, `PUT`, and `PATCH` typically include data in their body. ```http POST /places HTTP/1.1 Host: api.example.org Content-Type: application/json { "name": "High Wood", "lat": 50.464569783, "lon": -4.486597585 } ``` An example of a PATCH request following the [RFC 7386: JSON Merge Patch](https://datatracker.ietf.org/doc/html/rfc7386) format, which accepts just the fields the user wants to change: ```http PATCH /places/123 HTTP/1.1 Host: api.example.org Content-Type: application/merge-patch+json { "name": "Highwood" } ``` This request body is yours to do with as you want. There's a lot of freedom in how you structure your data, but there are some best practices to follow which you can learn more about in the [API Collections & Resources](/api-design api-collections) guide. ## Data formats This is generally where all of the domain-specific modelling will happen for data and workflows the API is meant to handle, but generally its not just spitting raw data around. The understand more about structuring request bodies, its important to understand [data formats](/api-design/data-formats). ## File uploads Instead of sending JSON data, files can be sent in the request body. Learn more about file uploads in the [File Uploads](/api-design/file-uploads) guide. ## Best Practices ### 1. Keep request & response data consistent Maintain the same structure for data going in and out of your API. You want to strive for predictability and consistency in your API design. When a user sends a `POST` request to create a new resource, they should get back a response that looks like the resource they just created. If a user updates a resource, the response should return the new state of the updated resource. ```json // POST Request { "name": "High Wood", "lat": 50.464569783, "lon": -4.486597585 } // Response { "id": 123, "name": "High Wood", "lat": 50.464569783, "lon": -4.486597585, "created_at": "2024-10-24T12:00:00Z" } ``` More on this in the [API Responses](/api-design/responses) guide. ### 2. Do not use request bodies for DELETE requests Some API designers use a request body for a DELETE request to capture more information about the deletion of a resource. Doing so is recommended against in both [RFC9110: HTTP Semantics](https://www.rfc-editor.org/rfc/rfc9110.html#name-delete) which states the semantics are undefined, and is warned against further in the OpenAPI specification. Some tooling will support it some wont. ```http DELETE /drinks/1 HTTP/1.1 Host: api.example.org Content-Type: application/json { "reason": "Weird combination of ingredients that proved unpopular with... everyone." } ``` Instead it would be more appropriate to create a new resource called `POST /drinks/1/cancellation` and essentially create a DrinkCancellation resource. This resource can then be used to track the reason for the cancellation, and any other information that might be useful, as well as avoiding the need for a request body on the DELETE request. ### 3. Use the correct content type The `Content-Type` header is used to indicate the media type of the resource being sent in the request body. The most common content types are: - `application/json`: JSON data - `application/xml`: XML data - `application/x-www-form-urlencoded`: Form data - `multipart/form-data`: File uploads Make sure to remind the user to set the `Content-Type` header in their request to the correct value, and if they send the wrong thing then return a `415 Unsupported Media Type` response. ### 4. Avoid optional request bodies When designing your API, avoid making request bodies optional. It's a little confusing, and harder for the user to understand what they need to send, harder to document, harder to validate, harder to generate code for, and harder to test. For example, if you have a `POST /loans/{id}/renewal` endpoint with an optional renewal date, instead of making the whole request body optional make it required and make the renewal date property optional. ```http POST /loans/123/renewal HTTP/1.1 Host: api.example.org Content-Type: application/json { "renewal_date": "2024-10-24" } ``` This way, the user always knows what to expect in the request body, and you can validate the request body more easily. If the user doesn't send a renewal date, you can just use the current date as the default value. This is a lot easier to understand and work with than having to check if the request body is present or not, and then check if the renewal date is present or not. # Designing API responses Source: https://speakeasy.com/api-design/responses import { Callout } from '@/mdx/components'; The API response is the most important part of the entire API. - Did the API consumer ask a question? They need that answer. - Did the API consumer ask to transfer £1,000,000? They need to be confident that went well. - Did the API consumer make a mistake? Tell them how to fix it so they can get back to making you money. Creating clear, consistent API responses is crucial for building usable APIs. This guide covers essential patterns and best practices for API responses. ## Anatomy of an API Response An API response is primarily made up of a status code, headers, and a response body, so let's look at each of those parts in turn. ### Headers Just like requests allow API consumers to add HTTP headers to act as metadata for the request, APIs and other network components can add headers to a response. ```http HTTP/2 200 OK Content-Type: application/json Cache-Control: public, max-age=18000 RateLimit: "default";r=50;t=30 { "title": "something" } ``` This is a successful request, with some JSON data as highlighted by the `Content-Type` header. The API has also alerted the API consumer that this is cacheable so they don't need to ask for it again for 5 hours, and explained that the client is running a little low on their rate limiting policy with only 50 more requests allowed in the next 30 seconds. API responses contain lots of useful metadata in the headers, but data is going to be in the response body. ### Response Body You should strive to keep response consistent and well-structured, with minimal nesting and correct use of data types. ```json { "id": 123, "name": "High Wood", "location": { "lat": 50.464569783, "lon": -4.486597585 }, "created_at": "2024-10-24T12:00:00Z", "links": { "reviews": "/places/123/reviews" } } ``` It's pretty common to add an `id` of some sort, often data will have dates, and relationships and available actions can be linked allowing API consumers to easily find related information without going on a scavenger hunt. ### Status Codes So far we've only looked at success, but how do we know if something has worked or not? You could look at the response body and try to figure it out, and for years people were doing silly things like setting `{ "success": true/false }` in their response body to give people a hint, but as always there's a far better way defined in the HTTP spec which covers loads more use-cases and works out of the box with loads of tools: HTTP Status Codes. A status code is a number and a matching phrase, like `200 OK` and `404 Not Found`. There are countless status codes defined in the HTTP RFCs and elsewhere, and some big companies have invented their own which became common conventions, so there's plenty to choose from. Arguments between developers will continue for the rest of time over the exact appropriate status code to use in any given situation, but these are the most important status codes to look out for in an API: #### 2XX is all about success Whatever the API was asked to do was successful, up to the point that the response was sent. A `200 OK` is a generic "all good", a `201 Created` means something was created, and a `202 Accepted` is similar but does not say anything about the actual result, it only indicates that a request was accepted and is being processed asynchronously. It could still go wrong, but at the time of responding it was all looking good at least up until it was put in the queue. The common success status codes and when to use them: * **200** - Generic everything is OK. * **201** - Created something OK. * **202** - Accepted but is being processed async (for a video means. encoding, for an image means resizing, etc.) * **204** - No Content but still a success. Ideal for a successful `DELETE` request, for example. Example success response ```http HTTP/1.1 200 OK Content-Type: application/json { "user": { "id": 123, "name": "John Doe" } } ``` #### 3XX is all about redirection These are all about sending the calling application somewhere else for the actual resource. The best known of these are the `303 See Other` and the `301 Moved Permanently`, which are used a lot on the web to redirect a browser to another URL. Usually a redirect will be combined with a `Location` header to point to the new location of the content. #### 4XX is all about client errors Indicate to your clients that they did something wrong. They might have forgotten to send authentication details, provided invalid data, requested a resource that no longer exists, or done something else wrong which needs fixing. Key client error codes: * **400** - Bad Request (should really be for invalid syntax, but some folks. use for validation). * **401** - Unauthorized (no current user and there should be). * **403** - The current user is forbidden from accessing this data. * **404** - That URL is not a valid route, or the item resource does not exist. * **405** - Method Not Allowed (your framework will probably do this for you.) * **406** - Not Acceptable (the client asked for a content type that the API does not support.) * **409** - Conflict (Maybe somebody else just changed some of this data, or status cannot change from e.g: "published" to "draft"). * **410** - Gone - Data has been deleted, deactivated, suspended, etc. * **415** - The request had a `Content-Type` which the server does not know how to handle. * **429** - Rate Limited, which means take a breather, sleep a bit, try again. #### 5XX is all about service errors With these status codes, the API, or some network component like a load balancer, web server, application server, etc. is indicating that something went wrong on their side. For example, a database connection failed, or another service was down. Typically, a client application can retry the request. The server can even specify when the client should retry, using a `Retry-After` HTTP header. Key server error codes: * **500** - Something unexpected happened, and it is the API's fault. * **501** - This bit isn't finished yet, maybe it's still in beta and you don't have access. * **502** - API is down, but it is not the API's fault. * **503** - API is not here right now, please try again later. As you can see, there are a whole bunch of HTTP status codes. You don't need to try and use them all, but it is good to know what they are and what they mean so you can use the right one for the job. You have two choices, either read the [full list of status codes from the IANA](https://www.iana.org/assignments/http-status-codes/http-status-codes.xhtml), or swing by [http.cats](http://http.cat/) and see what the cats have to say about it. ### Using Status Codes ```ts import axios, { AxiosError } from 'axios'; async function makeHttpRequest() { try { const response = await axios.get('https://example.com/api/resource'); console.log('Response:', response.data); } catch (error) { if (! axios.isAxiosError(error)) { console.error('An unexpected error occurred:', error); return; } const axiosError = error as AxiosError; if (axiosError.response?.status === 401) { console.error('You need to log in to access this resource.'); } else if (axiosError.response?.status === 403) { console.error('You are forbidden from accessing this resource.'); } else if (axiosError.response?.status === 404) { console.error('The resource you requested does not exist.'); } else { console.error('An error occurred:', axiosError.message); } } } makeHttpRequest(); ``` Now you can warn API consumers of fairly specific problems. Doing it way is cumbersome, but there's plenty of generic libraries with various extensions and "middlewares" that will help auto-retry any auto-retriable responses, automatically cache cachable responses, and so on. Avoid confusing your API consumers by enabling retry logic in your Speakeasy SDK. ## Best Practices ### 1. Keep Status Codes Appropriate & Consistent It's important to keep status codes consistent across your API, ideally across your entire organization. This is not just for nice feels, it helps with code reuse, allowing consumers to share code between endpoints, and between multiple APIs. This means they can integrate with you quicker, and with less code, and less maintenance overhead. ### 2. Keep Request & Response Bodies Consistent Sometimes API developers end up with divergent data models between the request and the response, and this should be avoided whenever possible. Whatever shape you pick for a request, you should match that shape on the response. ```json // POST /places { "name": "High Wood", "lat": 50.464569783, "lon": -4.486597585 } ``` ```json // GET /places/123 { "id": 123, "name": "High Wood", "lat": 50.464569783, "lon": -4.486597585, "created_at": "2024-10-24T12:00:00Z" } ``` You can see that some differences, like `id` or `created_at` dates on the response but not the request. That's OK, because they can be handled as "read-only" or "write-only" fields in the API documentation and generated code, meaning they are using the same models just ignoring a few fields depending on the context. The problem often comes from various clients having a word with the API developers about "helping them out", because some library being used by the iOS app would prefer to send coordinates as a string and they don't want to convert them to a decimal for some reason. Then the API team wanted to have the responses wrapped into objects to make it look tidy, but the React team said it would be too hard to get their data manager to do that, so the request skipped it. ```json // POST /places { "name": "High Wood", "lat": "50.464569783", "lon": "-4.486597585" } ``` ```json // GET /places/123 { "id": 123, "name": "High Wood", "location": { "lat": 50.464569783, "lon": -4.486597585 }, "created_at": "2024-10-24T12:00:00Z" } ``` Aghh! This sort of thing causes confusion for everyone in the process, and whilst any one change being requested might feel reasonable, when a few of them stack up the API becomes horrible to work with. Push back against request/response model deviations. It's not worth it. ### 3. Return detailed errors Just returning a status code and a message is not enough, at the bare minimum add an error message in the JSON body that adds more context. ``` HTTP/2 409 Conflict Content-Type: application/json { "error": "A place with that name already exists." } ``` This is better than nothing but not ideal. Other information needs to be added to help with debugging, and to help the API client differentiate between errors. There is a better way: [RFC 9457](https://tools.ietf.org/html/rfc9457) which defines a standard way to return errors in JSON (or XML). ```http HTTP/2 409 Conflict Content-Type: application/problem+json { "type": "https://api.example.com/probs/duplicate-place", "title": "A place with that name already exists.", "detail": "A place with the name 'High Wood' already exists close to here, have you or somebody else already added it?", "instance": "/places/123/errors/", "status": 409 } ``` More on this in the [API Errors](/api-design/errors) guide. ## Best Practices ### 1. Keep Status Codes Appropriate & Consistent It's important to keep status codes consistent across your API, ideally across your entire organization. This is not just for nice feels, it helps with code reuse, allowing consumers to share code between endpoints, and between multiple APIs. This means they can integrate with you quicker, and with less code, and less maintenance overhead. ### 2. Keep Request & Response Bodies Consistent Sometimes API developers end up with divergent data models between the request and the response, and this should be avoided whenever possible. Whatever shape you pick for a request, you should match that shape on the response. ```json // POST /places { "name": "High Wood", "lat": 50.464569783, "lon": -4.486597585 } ``` ```json // GET /places/123 { "id": 123, "name": "High Wood", "lat": 50.464569783, "lon": -4.486597585, "created_at": "2024-10-24T12:00:00Z" } ``` You can see that some differences, like `id` or `created_at` dates on the response but not the request. That's OK, because they can be handled as "read-only" or "write-only" fields in the API documentation and generated code, meaning they are using the same models just ignoring a few fields depending on the context. The problem often comes from various clients having a word with the API developers about "helping them out", because some library being used by the iOS app would prefer to send coordinates as a string and they don't want to convert them to a decimal for some reason. Then the API team wanted to have the responses wrapped into objects to make it look tidy, but the React team said it would be too hard to get their data manager to do that, so the request skipped it. ```json // POST /places { "name": "High Wood", "lat": "50.464569783", "lon": "-4.486597585" } ``` ```json // GET /places/123 { "id": 123, "name": "High Wood", "location": { "lat": 50.464569783, "lon": -4.486597585 }, "created_at": "2024-10-24T12:00:00Z" } ``` Aghh! This sort of thing causes confusion for everyone in the process, and whilst any one change being requested might feel reasonable, when a few of them stack up the API becomes horrible to work with. Push back against request/response model deviations. It's not worth it. ### 3. Return detailed errors Just returning a status code and a message is not enough, at the bare minimum add an error message in the JSON body that adds more context. ``` HTTP/2 409 Conflict Content-Type: application/json { "error": "A place with that name already exists." } ``` This is better than nothing but not ideal. Other information needs to be added to help with debugging, and to help the API client differentiate between errors. There is a better way: [RFC 9457](https://tools.ietf.org/html/rfc9457) which defines a standard way to return errors in JSON (or XML). ```http HTTP/2 409 Conflict Content-Type: application/problem+json { "type": "https://api.example.com/probs/duplicate-place", "title": "A place with that name already exists.", "detail": "A place with the name 'High Wood' already exists close to here, have you or somebody else already added it?", "instance": "/places/123/errors/", "status": 409 } ``` More on this in the [API Errors](/api-design/errors) guide. # Testing your API Source: https://speakeasy.com/api-design/testing import Image from "next/image"; We write tests to build trust. Trust that our software is reliable, safe, and extendable. When it comes to testing public APIs, robust testing gives users confidence that the API *behaves* as expected. This guide not only covers the *how* of API testing, but also how to project externally the confidence brought by testing. ## Why API testing matters API testing involves sending requests to your API endpoints and validating the responses. It's faster and more focused than UI testing, allowing for fast feedback, better coverage of edge cases and errors, and more stable verification of the contracts between system components. But for *public* or third-party-consumed APIs, the *why* extends further. Your tests aren't just for *you* to catch regressions. They are potentially the single most accurate, up-to-date source of truth about how your API actually behaves, especially in nuanced situations. ## The test pyramid does not apply to API testing API testing clashes with the traditional test pyramid - you know, the one that says, "Many unit tests at the bottom, fewer integration tests in the middle, and a few end-to-end tests at the top." The test pyramid This pyramid is a good model for verifying the implementation details of small units of work, but it's a terrible model for verifying an API's behavior. Unit tests, useful on the server when testing complex logic, don't tell us much about how the API behaves as a whole. We need a new model for API tests. ## The API test ~pyramid~ trapezoid The traditional test pyramid (many unit tests, fewer integration tests, fewest E2E tests) is okay for internal implementation details. But for verifying API *behavior* from a consumer's viewpoint, the emphasis shifts. The API test pyramid It's not exactly a pyramid, but you get the idea. At the base, we have **unit tests**. These are the tests that verify the internal workings of your API. They're important for maintaining code quality and catching regressions, but they don't tell us much about how the API behaves from a consumer's perspective, so we'll limit their scope and keep them private. In the middle, we've added **contract tests**. These tests verify that the API's contract with consumers is upheld. They're a step up from unit tests because they verify the API's behavior at its boundaries. We've covered contract testing in detail in a [previous post](https://www.speakeasy.com/post/contract-testing-with-openapi). We'll focus on **integration tests** at the top-middle layer of the pyramid. These tests verify the API's behavior from a consumer's perspective. They're the most relevant tests for API consumers because they show exactly how the API behaves in different scenarios. Finally, **end-to-end tests** sit at the top. These tests verify the API's behavior in a real-world scenario, often spanning multiple systems. They're useful from an API consumer's perspective, but they're also the most brittle and expensive to maintain (unless you're generating them, but we'll get to that). Let's focus on why we're advocating for making these integration tests public as part of your SDK. ## A practical guide to API integration testing We've established that integration tests are vital for verifying API behavior from a consumer's viewpoint - but how do you write good integration tests? Here's a practical approach: ### 1. Prerequisites Before you start, ensure you have: - **Access to a test environment:** Obtain access to a deployed instance of your API, ideally isolated from production (for example, staging or QA). This environment should have representative, non-production data. - **API credentials:** Acquire valid credentials (API keys, OAuth tokens) for accessing the test environment. Use dedicated test credentials, *never* production ones. - **A testing framework:** Choose a testing framework suitable for your language (for example, `pytest` for Python, `Jest`/`Vitest` for Node.js/TypeScript, `JUnit`/`TestNG` for Java, or `Go testing` for Go). - **An HTTP client or SDK:** Establish the way you'll make requests. While you can use standard HTTP clients (`requests`, `axios`, `HttpClient`), **we strongly recommend using your own SDK** for these tests. This validates the SDK itself and mirrors the consumer experience. ### 2. Test structure The Arrange-Act-Assert (AAA) pattern provides you with a clear, standardized approach to structuring your tests: - **Arrange:** Set up all preconditions for your test. This includes initializing the SDK client, preparing request data, and potentially ensuring the target test environment is in the correct state (for example, by checking that the required resources exist or *don't* exist). - **Act:** Execute the specific action you want to test. This is typically a single method call on your SDK that corresponds to an API operation. - **Assert:** Verify the outcome of the action. Check the response status, headers, and body content. Assert any expected side effects if applicable. You can also include a **Teardown** step (often via `afterEach`, `afterAll`, or fixture mechanisms in testing frameworks) to clean up any resources created during the test to ensure your tests don't interfere with one another. Here's an example using TypeScript with Vitest, testing the Vercel SDK generated by Speakeasy. Notice how the code maps to the AAA pattern: ```typescript import { expect, test } from "vitest"; import { Vercel } from "../index.js"; // Assuming SDK is in ../index.js import { createTestHTTPClient } from "./testclient.js"; // Helper for test client test("User Request Delete - Happy Path", async () => { // --- ARRANGE --- const testHttpClient = createTestHTTPClient("requestDelete"); const vercel = new Vercel({ serverURL: process.env["TEST_SERVER_URL"] ?? "http://localhost:18080", httpClient: testHttpClient, bearerToken: process.env["VERCEL_TEST_TOKEN"] ?? "", }); const requestParams = {}; // No specific parameters needed for this call // --- ACT --- const result = await vercel.user.requestDelete(requestParams); // --- ASSERT --- expect(result).toBeDefined(); // Example: Asserting specific fields in the response. // Again, the exact values here might be due to the testHttpClient. // In live tests, you might check `result.id` exists and is a string, // `result.email` matches a known test user, etc. expect(result).toEqual({ id: "", // Placeholder suggests fixed response email: "Lamont82@gmail.com", // Placeholder suggests fixed response message: "Verification email sent", }); }); ``` In this example, the AAA comments make the test flow obvious to someone reading the tests. Because the test uses the [Vercel SDK](https://www.npmjs.com/package/@vercel/sdk), the level of abstraction matches that of a developer using the SDK. ### 3. Selecting scenarios to test When deciding which tests to write, make sure you cover happy paths and edge cases, avoiding the temptation to write tests only for the way your API was "meant to be used". - **Happy paths:** Verify the core functionality works as expected with valid inputs, for example, by checking that it can list resources or retrieve a specific item. ```typescript // Example: Listing deployments (happy path) test('should list deployments for a project successfully', async () => { // Arrange const vercel = new Vercel(/* ... setup client ... */); const projectId = 'prj_testProject123'; // Assume this project exists in test env const listParams = { projectId: projectId, limit: 5 }; // Act const result = await vercel.deployments.list(listParams); // Assert expect(result).toBeDefined(); expect(result.pagination).toBeDefined(); expect(result.deployments).toBeInstanceOf(Array); // Add more specific assertions if needed, e.g., checking deployment properties if (result.deployments.length > 0) { expect(result.deployments[0].uid).toBeDefined(); expect(result.deployments[0].state).toBeDefined(); } }); ``` - **Error handling:** Test how the API responds to invalid scenarios. Use `try...catch` or framework-specific methods (`expect().rejects`) to verify error responses. ```typescript // Example: Getting a non-existent project (404 Not Found) test('should return 404 when getting a non-existent project', async () => { // Arrange const vercel = new Vercel(/* ... setup client ... */); const nonExistentProjectId = 'prj_doesNotExistXYZ'; // Act & Assert try { await vercel.projects.get(nonExistentProjectId); // If the above line doesn't throw, the test fails throw new Error('API call should have failed with 404'); } catch (error) { // Assuming the SDK throws a specific error type with status code expect(error).toBeInstanceOf(ApiError); // Replace ApiError with actual error type expect(error.statusCode).toBe(404); // Optionally check error code or message from the API response body // expect(error.body.error.code).toBe('not_found'); } // Alternative using Vitest/Jest 'rejects' matcher: // await expect(vercel.projects.get(nonExistentProjectId)) // .rejects.toThrow(ApiError); // Or specific error message/type // await expect(vercel.projects.get(nonExistentProjectId)) // .rejects.toHaveProperty('statusCode', 404); }); // Example: Missing permissions (403 Forbidden) test('should return 403 when attempting action without permissions', async () => { // Arrange // Assume 'readOnlyVercelClient' is configured with a token that only has read permissions const readOnlyVercelClient = new Vercel(/* ... setup with read-only token ... */); const projectIdToDelete = 'prj_testProject123'; // Act & Assert await expect(readOnlyVercelClient.projects.delete(projectIdToDelete)) .rejects.toThrow(/* Specific Error Type or Message Indicating Forbidden */); await expect(readOnlyVercelClient.projects.delete(projectIdToDelete)) .rejects.toHaveProperty('statusCode', 403); }); ``` - **Edge cases:** Test boundary conditions like pagination limits or the handling of optional parameters. ```typescript // Example: Testing pagination limits (listing user events) test('should respect pagination limit parameter', async () => { // Arrange const vercel = new Vercel(/* ... setup client ... */); const limit = 5; // Request a small limit // Act // Use the listUserEvents endpoint from the previous example const result = await vercel.user.listUserEvents({ limit: limit }); // Assert expect(result).toBeDefined(); expect(result.events).toBeInstanceOf(Array); // Check if the number of returned events is less than or equal to the limit expect(result.events.length).toBeLessThanOrEqual(limit); expect(result.pagination).toBeDefined(); // Maybe check pagination 'next' value based on expected total results }); test('should handle requests with only optional parameters', async () => { // Arrange const vercel = new Vercel(/* ... setup client ... */); // Act: Call an endpoint with only optional query params, e.g., listing projects without filters const result = await vercel.projects.list({}); // Pass empty object or omit params // Assert expect(result).toBeDefined(); expect(result.projects).toBeInstanceOf(Array); // Assert default behavior when no filters are applied }); ``` - **Business logic:** Verify specific rules unique to your domain (this depends heavily on the API's features). ```typescript // Example: Verifying default deployment domain structure test('should create a deployment with the correct default domain format', async () => { // Arrange const vercel = new Vercel(/* ... setup client ... */); const projectId = 'prj_testProject123'; // Simplified deployment request data const deploymentData = { name: 'my-test-app', files: [/* ... file data ... */] }; // Act: Create a new deployment (assuming a method exists) // Note: Real deployment creation is complex, this is illustrative // const deployment = await vercel.deployments.create(projectId, deploymentData); // Let's assume we list deployments instead to find the latest one const deploymentsResult = await vercel.deployments.list({ projectId: projectId, limit: 1}); const latestDeployment = deploymentsResult.deployments[0]; // Assert: Check if the autogenerated domain follows the expected pattern expect(latestDeployment).toBeDefined(); expect(latestDeployment.url).toBeDefined(); // Example assertion: checks if URL matches Vercel's typical preview URL pattern expect(latestDeployment.url).toMatch(/^my-test-app-\w+\.vercel\.app$/); // Or check against a specific alias rule if applicable }); ``` - **Authentication:** Test different auth scenarios. ```typescript // Example: Using an invalid bearer token (401 Unauthorized) test('should return 401 Unauthorized with invalid token', async () => { // Arrange const invalidToken = 'invalid-bearer-token'; const vercelInvalidAuth = new Vercel({ serverURL: process.env["TEST_SERVER_URL"] ?? "http://localhost:18080", // httpClient: createTestHTTPClient(...), // Use real client for auth tests bearerToken: invalidToken, }); // Act & Assert // Attempt any API call, e.g., getting user info await expect(vercelInvalidAuth.user.get()) .rejects.toThrow(/* Specific Error for Unauthorized */); await expect(vercelInvalidAuth.user.get()) .rejects.toHaveProperty('statusCode', 401); }); // Example: Successful auth (implicitly tested in happy path tests) test('should succeed with a valid bearer token', async () => { // Arrange const vercel = new Vercel(/* ... setup with VALID token ... */); // Act const user = await vercel.user.get(); // Assert expect(user).toBeDefined(); expect(user.id).toBeDefined(); expect(user.email).toBeDefined(); // Assuming user object has email }); ``` ### 4. Managing state and dependencies - **Isolation:** Aim for independent tests. Avoid tests that rely on the state left by a previous test. Use `beforeEach`/`afterEach` or test fixtures to set up and tear down state. - **Test Data:** Use dedicated test accounts and seed data. Generate unique identifiers (like UUIDs or timestamped names) within tests to prevent collisions. - **Cleanup:** Implement cleanup logic to remove created resources. This is important for keeping the test environment clean and tests repeatable. ### 5. Running tests - **Locally:** Run tests frequently during development against your local or a dedicated dev environment. - **CI/CD:** Integrate tests into your CI/CD pipeline. Run them automatically on every commit or pull request against a staging environment *before* deploying to production. By following these steps and focusing on testing through your SDK, you'll build a strong test suite that verifies your API's *actual behavior* from the perspective of your consumers. These are precisely the kinds of tests - written using the SDK - that provide immense value when shared publicly. ## Publishing Tests When your API is a black box, every consumer pays a "reverse engineering tax" while they need to rediscover knowledge that you already have. If you have the knowledge internally, save your API consumers the trouble and share it with them. API-first companies are already following this approach. Stripe, for example, maintains extensive test fixtures and behavioral tests as part of their SDKs. These tests serve as both verification and documentation, showing exactly how their APIs respond in various scenarios. Here's what belongs in the public domain: ✅ **Authentication flow verification:** Tests that demonstrate how authentication works, covering token acquisition, refresh flows, and error handling. ```python filename="auth_flow_test.py" def test_invalid_api_key_returns_401(): client = ApiClient(api_key="invalid_key") response = client.users.list() assert response.status_code == 401 assert response.json()["error"] == "unauthorized" assert "invalid_api_key" in response.json()["error_code"] ``` ✅ **Rate limit behavior tests:** Tests that verify how your API behaves when rate limits are approached or exceeded. ```python filename="rate_limit_test.py" def test_rate_limit_headers_present(): response = client.resources.list() assert "X-RateLimit-Limit" in response.headers assert "X-RateLimit-Remaining" in response.headers assert "X-RateLimit-Reset" in response.headers ``` ✅ **Error condition handling:** Tests that demonstrate how your API responds to different error states, such as invalid inputs, missing resources, and service errors. ```python filename="error_handling_test.py" def test_resource_not_found(): response = client.products.get(id="nonexistent") assert response.status_code == 404 assert response.json()["error"] == "not_found" assert response.json()["resource_type"] == "product" ``` ✅ **State transition tests:** Tests that verify how resources change state between API operations. ```python filename="order_transition_test.py" def test_order_transition_from_pending_to_processing(): # Create order order = client.orders.create({"items": [{"product_id": "123", "quantity": 1}]}) assert order.status == "pending" # Process payment payment = client.payments.create({"order_id": order.id, "amount": order.total}) # Check updated order updated_order = client.orders.get(order.id) assert updated_order.status == "processing" ``` ✅ **Complex business logic validation:** Tests that verify domain-specific rules and constraints. ```python filename="discount_test.py" def test_discount_applied_only_to_eligible_items(): # Create cart with mixed eligible and non-eligible items cart = client.carts.create({ "items": [ {"product_id": "eligible-123", "quantity": 2}, {"product_id": "non-eligible-456", "quantity": 1} ] }) # Apply discount updated_cart = client.carts.apply_discount(cart.id, {"code": "SUMMER10"}) # Verify discount only applied to eligible items eligible_items = [i for i in updated_cart.items if i.product_id.startswith("eligible")] non_eligible_items = [i for i in updated_cart.items if i.product_id.startswith("non-eligible")] for item in eligible_items: assert item.discount_applied == True for item in non_eligible_items: assert item.discount_applied == False ``` Here's what should remain private: ❌ **SDK implementation unit tests:** Tests that verify specific SDK implementation details or internal methods. ❌ **SDK build verification:** Tests that ensure your SDK builds correctly on different platforms or versions. ❌ **Internal platform tests:** Tests that verify behavior of internal services or dependencies. ❌ **SDK compatibility checks:** Tests that verify compatibility with different language versions or environments. The distinction comes down to this: If the test verifies behavior that your API consumers need to understand, it should be public. If it verifies internal implementation details that could change without affecting the API's external behavior, it should remain private. This separation creates a clean boundary between what's public and what's private: | Public tests (ship these) | Private tests (keep these) | |---------------------------|----------------------------| | Verify API behavior | Verify implementation details | | Written from consumer perspective | Written from maintainer perspective | | Stable as long as the API is stable | May change with internal refactoring | | Serve as executable documentation | Serve as implementation verification | | Focus on what happens at API boundaries | Focus on internal components | By making this distinction clear, you can confidently share the tests that provide value to your consumers while maintaining the freedom to change your implementation details privately. ### What is gained by sharing your API tests? When you publish your API behavior tests, you transform your internal verification tools into living documentation that can never go stale. Unlike traditional documentation, tests fail loudly when they don't match reality. This creates a powerful guarantee for your users - if the tests pass, the documented behavior is accurate. Public tests create a cycle of trust and quality: 1. **Improved API design:** When you know your tests will be public, you design more thoughtfully. 2. **Higher test quality:** Public scrutiny leads to better, higher-quality tests. 3. **Reduced support burden:** Users can answer their own questions by examining tests. 4. **Faster integration:** Developers can understand behavior more quickly and completely. 5. **Increased trust:** Transparent verification builds confidence in your API. The very act of preparing tests for public consumption forces a level of clarity and quality that might otherwise be neglected. ## Further testing considerations While sharing your API's behavioral tests provides tremendous value, there are several complementary testing approaches worth researching: ### Contract testing Investigate tools like [Pact](/post/pact-vs-openapi) that formalize the provider-consumer contract. These approaches complement public tests by allowing consumers to define their expectations explicitly. ### Chaos testing Research how companies like Netflix use [chaos engineering](https://en.wikipedia.org/wiki/Chaos_engineering) principles for API resilience. Deliberately introducing failures helps verify how your API behaves under unexpected conditions - which provides invaluable knowledge for your consumers. ### Performance test benchmarks Consider publishing performance benchmarks alongside behavioral tests. These reveal important scaling characteristics like throughput limits and latency under various loads, which impact consumer application design. Sharing performance tests in full may be risky due to the potential for misuse, but sharing high-level results and methodologies can still provide valuable insights. ### Security testing frameworks Explore frameworks like [OWASP ZAP](https://www.zaproxy.org/) that can verify API security controls. While you shouldn't publish vulnerability tests, sharing your approach to security verification builds trust. ### Consumer-driven testing Research how companies implement consumer-driven tests where API consumers contribute test cases representing their usage patterns. This collaborative approach strengthens the relationship between provider and consumer. Consumer-driven testing overlaps with contract testing, but it emphasizes the consumer's perspective more directly. ### Snapshot testing Look into [snapshot testing](https://vitest.dev/guide/snapshot) to detect unintended changes in API responses. These tests can serve as early warnings for breaking changes that might affect consumers. ### Testing in production Investigate techniques like [feature flags](https://launchdarkly.com/), [canary releases](https://martinfowler.com/bliki/CanaryRelease.html), and synthetic testing that extend verification into production environments. # Versioning and evolution Source: https://speakeasy.com/api-design/versioning Once an API has been designed, built, deployed, and integrated with by various consumers, changing the API can be very difficult. The scale of difficulty depends on the type of changes: - **Additive changes:** Adding new endpoints, adding new properties to a response, and introducing optional parameters are non-breaking changes that can typically be implemented without significant issues. - **Breaking changes:** Removing or renaming endpoints, removing required fields, and changing response structures are considered breaking changes. These have the potential to disrupt existing client applications, resulting in errors and loss of functionality. Clients, especially paying customers, may face the expensive and time-consuming task of adapting their code to accommodate breaking changes. For effective management of changes, developers must navigate the versioning and evolution of APIs carefully, ensuring that client integrations aren't negatively impacted. Let's explore these challenges in more detail. ## When API changes are an issue Some APIs are built purely to service a single application. For example, an API might be the backend for a web application handled by a single full-stack developer or by a team that manages both the frontend and the API. In this case, changes wouldn't be problematic because both the frontend and backend could be updated simultaneously, ensuring that there's no risk of breaking integration. However, in most cases APIs are consumed by multiple clients, ranging from other teams within the same organization to external customers. This introduces complexities: - **Internal clients:** Even when the consumers are within the same organization, changes may require coordination, especially if the API is used by different teams working on separate services. The timing of updates and changes can cause delays or disruptions. - **External clients:** If the API is being used by third-party clients, particularly paying customers, changes can become even more difficult. External clients may resist updates due to the effort and risk involved in modifying their integrations. A major change could result in lost business, dissatisfaction, or churn. When API consumers aren't in sync with the development team, managing versioning becomes essential to ensuring smooth transitions. ## Why APIs need to change It's unlikely that anyone has ever released software and thought, "That's perfect, no change needed." APIs evolve over time like any other software, whether it's due to changing business requirements, feedback from users, or the need to adopt new technologies. APIs are rarely "perfect" and immutable. Just like software, APIs require a versioning system to accommodate changes. Developers use version numbers to signify changes in the API contract, allowing consumers to choose which version of the API they wish to use. This ensures backward compatibility for existing clients while introducing improvements and fixes in newer versions. With most software, users can have any version running, resulting in multiple versions of the software running on various users' computers at once. Common conventions, including [Semantic Versioning](https://semver.org/), use three numbers: major, minor, and patch. So, some users might be running 1.0.0 while others run 1.0.2, and eventually, some may run 2.1.3. A breaking change might look like: - A change to the endpoint structure - Adding a new required field - Removing a field from a response - Changing the behavior of an endpoint - Changing validation rules - Modifying the response format (for example, implementing a standard data format like JSON:API) If any of this is done, a new API version may be required to avoid breaking existing clients. ## Versioning an API API versioning involves assigning a version number or identifier to the API, essentially creating multiple different APIs that are segmented in some clear way. Versioning allows consumers to specify which version of the API they wish to interact with. There are countless ways people have tried to solve this problem over time, but there are two main approaches: ### URL versioning One of the most common approaches, URL versioning, segments the API by including a version number in the URL. Typically, only a major version is used, as seen in this example: ```http GET https://example.com/api/v1/users/123 ``` ```json { "id": 123, "first_name": "Dwayne", "last_name": "Johnson" } ``` In this example, `v1` refers to the version of the API. This initial version preserves the way the resource was designed at first. As the API grows, a new version is introduced to accommodate changes, separate from the first version. ```http GET https://example.com/api/v2/users/3a717485-b81b-411c-8322-426a7a5ef5e6 ``` ```json { "id": 123, "full_name": "Dwayne Johnson", "preferred_name": "The Rock" } ``` Here, the v2 endpoint introduces a few notable changes: - The developers phased out auto-incrementing IDs, as per the [security advice](/api-design/security). - They ditched the [fallacy of people having two names](https://www.kalzumeus.com/2010/06/17/falsehoods-programmers-believe-about-names/). - They helped users enter a preferred name to show publicly, instead of forcing everyone to publicize their legal name. Great, but this is a big change. If these changes were deployed on the `/v1` version, they would have broken most clients' usage: Users would see `404` errors using their old IDs, and the field changes would cause validation failures. As the changed API runs simultaneously under `/v2`, both versions can be used at once. This allows clients to migrate at their own pace and developers to update the API without breaking existing clients. ### Media-type versioning Instead of embedding the version number in the URL, media-type versioning places the versioning information in the HTTP `Accept` header. This allows for more flexible management of the API contract without altering the URL structure. ```http GET https://example.com/api/users/123 Accept: application/vnd.acme.v2+json ``` ```json { "id": 123, "full_name": "Dwayne Johnson", "preferred_name": "The Rock" } ``` In this case, the client specifies the version they want by including the `Accept` header with the version identifier (for example, `application/vnd.acme.v2+json`). The advantage is that the API URL remains clean, and the versioning is managed through the HTTP headers. This approach is less common than URL versioning and has a few downsides. It's a bit more complex to implement, and it's not as easy for clients to see which version of the API they're using. ## API evolution as an alternative While versioning is a popular solution to managing API changes, **API evolution** is an alternative that focuses on maintaining backward compatibility and minimizing breaking changes. Instead of introducing entirely new versions, API developers evolve the existing API to accommodate new requirements, but do so in a way that doesn't disrupt clients. API evolution is the concept of striving to maintain the "I" in API - interface elements like the request/response body, query parameters, and general functionality - only breaking them when absolutely necessary. It's the idea that API developers bend over backwards to maintain a contract, no matter how annoying that might be. It's often more financially and logistically viable for API developers to bear this load than to dump the workload on a wide array of consumers. ### API evolution in practice To work on a realistic example, consider this simple change: > The property `name` exists, and needs to be split into `first_name` and `last_name` to support Stripe's name requirements. It's a minor example, but removing `name` and requiring all consumers to change their code to use the two new fields would be a breaking change. Let's see how we can retain backward compatibility. Most web application frameworks commonly used to build APIs have a feature like "serializers", where database models are turned into JSON objects to be returned with all sensitive fields removed and any relevant tweaks or structure added. The database might have changed to using `first_name` and `last_name`, but the API does not need to remove the `name` property. Instead, `name` can be replaced with a "dynamic property" that joins the first and last names together and is then returned in the JSON. ```ruby class UserSerializer include FastJsonapi::ObjectSerializer attributes :name, :first_name, :last_name "#{object.first_name} #{object.last_name}" end end ``` ```http GET https://api.example.com/users/123 ``` ```json { "id": 123, "name": "Dwayne Johnson", "first_name": "Dwayne", "last_name": "Johnson" } ``` When a `POST` or `PATCH` is sent to the API, the API doesn't need to think about a version number to notice whether `name` has been sent. If `name` was sent, it can split the property, and if `first_name` and `last_name` were sent, it can handle the properties as expected. ```ruby class User < ApplicationRecord def name=(name) self.first_name, self.last_name = name.split(' ', 2) end end ``` A lot of changes can be handled by introducing new properties and supporting old properties indefinitely, but at a certain point, maintaining becomes cumbersome enough to require a bigger change. When an endpoint is starting to feel clunky and overloaded or fundamental relationships change, developers can avoid an API rewrite by evolving the API with new resources, collections, and relationships. ### Changing the domain model Let's consider the case of [Protect Earth](https://www.protect.earth/), a reforestation and rewilding charity with a Tree Tracker API that was in need of some fundamental changes. It used to focus on tracking the trees that were planted by recording a photo, coordinates, and other metadata to allow for sponsoring and funding tree planting. The API originally had a `/trees` resource as well as an `/orders` resource that had a `plantedTrees` property. However, the charity expanded beyond planting trees to sowing wildflower meadows, rewetting peat bogs, and clearing invasive species. Instead of adding `/peat` and `/meadows` resources, the API became more generic with a `/units` collection. Removing `/trees` or `plantedTrees` would break customers and stem the flow of funding. API evolution focuses on adding new functionality without breaking existing clients, so instead of removing the `/trees` endpoint, the API now supports both `/units` and `/trees`, with the `/trees` resource simply filtering the `/units` based on the `type` field: ```http GET https://api.protect.earth/trees ``` ```json { "id": 123, "species": "Oak", "location": { "latitude": 42.0399, "longitude": -71.0589 } } ``` ```http GET https://api.protect.earth/units ``` ```json { "id": 123, "type": "tree", "species": "Oak", "location": { "latitude": 42.0399, "longitude": -71.0589 } } ``` This change allows existing developers to continue using the `/trees` endpoint while new developers can use the `/units` endpoint. The API evolves to support new functionality without breaking existing clients. What about the `/orders` that contain `plantedTrees`? Removing this property would be a breaking change, so Protect Earth needs a backward-compatible solution. With API evolution, there are countless options. It's possible to add both an old property and a new property, allowing clients to migrate at their own pace. For example, we could add a new `allocatedUnits` property to the `/orders` resource, while keeping the old `plantedTrees` property: ```http GET https://api.protect.earth/orders/abc123 ``` ```json { "id": "abc123", "organization": "Protect Earth", "orderDate": "2025-01-21", "status": "fulfilled", "plantedTrees": [ { "id": 456, "species": "Oak", "location": { "latitude": 42.0399, "longitude": -71.0589 } } ], "allocatedUnits": [ { "id": 456, "type": "tree", "species": "Oak", "location": { "latitude": 42.0399, "longitude": -71.0589 } } ] } ``` However, for orders with 20,000 trees, this change means there would be 40,000 items across two almost identical sub-arrays. This is a bit of a waste, but really, it highlights an existing design flaw. Why are these sub-arrays not [paginated](/api-design/pagination)? And why are units embedded inside orders? Units and orders are different resources, and it's far easier to treat them as such. API evolution gives us a chance to fix this. There is already a `/units` endpoint, so let's use that. ```http GET https://api.protect.earth/orders/abc123 ``` ```json { "id": "abc123", "organization": "Protect Earth", "orderDate": "2025-01-21", "status": "fulfilled", "unitType": "peat", "links": { "units": "https://api.protect.earth/units?order=abc123" } } ``` This way, the `/order` resource is just about the order, and the `/units` resource is about the units. This is a more RESTful design, and it's a better way to handle the relationship between orders and units. Where did the `plantedTrees` property go? It was moved behind a switch and will only show up on orders for trees. All other unit types can be found on the `/units` link, which benefits from full pagination. ### Deprecating endpoints All this flexibility comes with a tradeoff: It's more work to maintain two endpoints, because there may be performance tweaks and bug reports that need to be applied to both. It's also more work to document and test both endpoints, so it's a good idea to keep an eye on which endpoints are being used and which aren't, and to remove the old ones when they're no longer needed. Old endpoints can be deprecated using the `Sunset` header. ```http HTTP/2 200 OK Sunset: Tue, 1 Jul 2025 23:59:59 GMT ``` Adding a `Sunset` header to `/trees` communicates to API consumers that the endpoint will be removed. If this is done with sufficient warning and with a clear migration path, it can lead to a smooth transition for clients. Further details can be provided in the form of a URL in a `Link` header and the `rel="sunset"` attribute. ``` HTTP/2 200 OK Sunset: Tue, 1 Jul 2025 23:59:59 GMT Link: ; rel="sunset" ``` The `Link` URL could direct users to a blog post or an upgrade guide in your documentation. ### Deprecating properties Deprecating properties is more challenging and generally best avoided whenever possible. You can't use `Sunset` to communicate that a property is being deprecated, because it only applies to endpoints. However, OpenAPI can help. The OpenAPI Specification version 3.1 added the `deprecate` keyword to allow API descriptions to communicate deprecations as an API evolves. ```yaml components: schemas: Order: type: object properties: plantedTrees: type: array items: $ref: '#/components/schemas/Tree' deprecate: true description: > An array of trees that have been planted, only on tree orders. *Deprecated:* use the units link relationship instead. ``` This change shows up in the documentation and can be used by SDKs to warn developers that they're using a deprecated property. Protect Earth could remove the `plantedTrees` property from the API entirely, but that would cause a breaking change, and it's best to avoid breaking changes whenever possible. A better option is to stop putting the `plantedTrees` property in new orders, starting on the deprecated date, and to leave it on older orders. Protect Earth is also adding the concept of orders expiring to its API. To prevent wasting unnecessary emissions, companies need to get their data out of the API within six months, or their information will be archived. If `plantedTrees` isn't added to new orders, and old orders are archived after six months, `plantedTrees` will eventually disappear completely and can then be removed from code. ### API design-first reduces change later Some APIs have kept their v1 APIs going for over a decade, which suggests they probably didn't need API versioning in the first place. Some APIs are on v14 because the API developers didn't reach out to any stakeholders to ask what they needed out of their API. They just wrote loads of code, rushing to rewrite it every time a new consumer came along with slightly different needs, instead of finding a solution that worked for everyone. Doing more planning, research, upfront API designing, and prototyping can cut out the need for the first few versions, as many initial changes are the result of not getting enough user and market research done early on. This is common in startups that are moving fast and breaking things, but it can happen in businesses of all sizes. ## Summary When it comes to deciding between versioning and evolution, consider how many consumers will need to upgrade, and how long that work is likely to take. If it's two days of work, and there are 10 customers, then that's 160 person-hours. With 1,000 customers, that's 16,000 person-hours. At a certain point, it becomes unconscionable to ask paying customers to do that much work, and it's better to see whether the update could be handled with a new resource, new properties, or other backward-compatible changes that slowly phase out their older forms over time, even if it's more work. # 100x-token-reduction-dynamic-toolsets Source: https://speakeasy.com/blog/100x-token-reduction-dynamic-toolsets import { CalloutCta } from "@/components/callout-cta"; import { GithubIcon } from "@/assets/svg/social/github"; } title="Gram OSS Repository" description="Check out Github to see how it works under the hood, contribute improvements, or adapt it for your own use cases. Give us a star!" buttonText="View on GitHub" buttonHref="https://github.com/speakeasy-api/gram" /> We shipped a major improvement to Gram that reduces token usage for MCP servers by 100x (or more) while maintaining consistent performance as toolset size grows. With dynamic toolsets, you can now build MCP servers with hundreds of tools without overwhelming the LLM's context window. Our benchmarks show that traditional MCP servers with 400 tools consume over 400,000 tokens before the LLM processes a single query. This is completely intractable (consider that Claude Code's maximum context window is 200,000 tokens). With dynamic toolsets, that same server uses just a few thousand tokens initially, with tools discovered only as needed. More importantly, this efficiency remains constant even as toolsets scale from 40 to 400+ tools. ## The problem with static toolsets When you connect an MCP server to an AI agent like Claude, every tool's schema is loaded into the context window immediately. For small servers with 10-20 tools, this works fine. But as your tool count grows, token usage explodes. Consider a general purpose MCP server for a large enterprise product with hundreds of tools. With a static approach, you're looking at 405,000 tokens consumed before any work begins. Since Claude's context window is 200,000 tokens, this server simply won't work. Even if it did fit, you'd be wasting most of your context on tools the LLM will never use for a given task. ## Dynamic toolsets: Two approaches We've implemented two experimental approaches to dynamic tool discovery, both of which dramatically reduce token usage while maintaining full functionality. ### Progressive search Progressive search uses a hierarchical discovery approach. Instead of exposing all 400 tools at once, we compress them into three meta-tools that the LLM can use to progressively discover what it needs: **`list_tools`** allows the LLM to discover available tools using prefix-based lookup. For example, `list_tools(/hubspot/deals/*)` returns only tools related to HubSpot deals. The tool description includes the structure of available sources and tags, creating a hierarchy that guides discovery. **`describe_tools`** provides detailed information about specific tools, including input schemas. We keep this separate from `list_tools` because schemas represent a significant portion of token usage. This separation optimizes context management at the cost of requiring an additional tool call. **`execute_tool`** runs the discovered and described tools as needed. With progressive search, our 400-tool server uses just 3,000 tokens initially and 3,000 additional tokens to complete a simple query like "List 3 HubSpot deals." That's a total of 6,000 tokens compared to 405,000 with the static approach. ### Semantic search Semantic search provides an embeddings-based approach to tool discovery. We create embeddings for all tools in advance, then search over them to find relevant tools for a given task. **`find_tools`** executes semantic search over embeddings created from all tools in the toolset. The LLM describes what it wants to accomplish in natural language, and the search returns relevant tools. This is generally faster than progressive search, especially for large toolsets, but may have less complete coverage since the LLM has no insight into what tools are available broadly. **`execute_tool`** runs the tools found through semantic search. With semantic search, our 400-tool server uses just 2,000 tokens initially and 3,000 additional tokens to complete the same query. That's a total of 5,000 tokens, making it slightly more efficient than progressive search for this use case. ## Performance comparison We conducted preliminary performance testing across toolsets of varying sizes (40, 100, 200, and 400 tools) with both simple and complex tasks. The results demonstrate that dynamic toolsets not only reduce initial token usage but also maintain consistent performance as toolset size grows. ### Token usage and costs We used Claude Code (Sonnet 4) to test the performance of dynamic toolsets. | Strategy / Toolset Size | Initial Tokens | Simple Task Tokens | Simple Task Cost | Complex Task Tokens | Complex Task Cost | | ----------------------- | -------------- | ------------------ | ---------------- | ------------------- | ----------------- | | **40 tools** | | | | | | | Static | 43,300 | 1,000 | $0.045 | 1,300 | $0.146 | | Progressive | 1,600 | 1,800 | $0.072 | 2,800 | $0.051 | | Semantic | 1,300 | 2,700 | $0.050 | 22,600 | $0.065 | | **100 tools** | | | | | | | Static | 128,900 | 1,300 | $0.373 | 1,300 | $0.155 | | Progressive | 2,400 | 2,700 | $0.076 | 6,600 | $0.096 | | Semantic | 1,300 | 4,300 | $0.053 | 12,200 | $0.134 | | **200 tools** | | | | | | | Static | 261,700 | — | — | — | — | | Progressive | 2,500 | 2,900 | $0.077 | 5,300 | $0.098 | | Semantic | 1,300 | 4,000 | $0.071 | 26,300 | $0.126 | | **400 tools** | | | | | | | Static | 405,100 | — | — | — | — | | Progressive | 2,500 | 2,700 | $0.078 | 5,700 | $0.099 | | Semantic | 1,300 | 3,400 | $0.069 | 8,300 | $0.160 | _Note: Static toolsets with 200 and 400 tools exceeded Claude Code's 200k context window limit and could not complete tasks._ ### Key insights The data reveals several critical advantages of dynamic toolsets: **Consistent scaling**: Dynamic toolsets maintain relatively constant token usage and costs even as toolset size quadruples from 100 to 400 tools. Progressive search uses ~2,500 initial tokens regardless of toolset size, while semantic search remains at just 1,300 tokens. **Cost escalation with static toolsets**: For smaller toolsets where static approaches work, costs escalate dramatically with multiple tool calls. The base cost is similar, but static toolsets send the entire tool context with every completion request. **Task complexity handling**: Both dynamic approaches handle complex multi-step tasks effectively, with progressive search showing particularly stable token usage patterns across different task complexities. ### Scaling behavior One of the most significant findings is how differently static and dynamic toolsets scale: **Static toolsets**: Initial token usage grows linearly with toolset size (405k tokens for 400 tools vs 43k for 40 tools). Beyond 200 tools, they exceed context window limits entirely. **Dynamic toolsets**: Initial token usage remains essentially flat. Progressive search uses 1,600-2,500 tokens regardless of whether the toolset has 40 or 400 tools. Semantic search is even more consistent at just 1,300 tokens across all sizes. This scaling difference means that dynamic toolsets become exponentially more efficient as APIs grow larger, making them essential for comprehensive API coverage. ## Key benefits Dynamic toolsets unlock several important capabilities: **Support for very large APIs**: You can now build MCP servers for APIs with hundreds or thousands of operations without hitting context limits. This makes it practical to expose comprehensive API functionality to AI agents. **Efficient context usage**: Only the tools actually needed for a task consume tokens. If an agent needs to work with HubSpot deals, it doesn't need to load schemas for Dub links or any other unrelated tools. **Faster response times**: Less context to process means faster initial responses. Semantic search is particularly fast since it requires fewer tool calls than progressive search. **Predictable costs**: While static toolsets show dramatic cost increases with multiple operations (up to 8x higher for complex tasks), dynamic toolsets maintain consistent pricing regardless of toolset size. **Better scaling**: As your API grows, dynamic toolsets scale naturally. Adding 100 new endpoints doesn't increase initial token usage at all. ## Technical implementation Both approaches work by exposing meta-tools that handle discovery rather than exposing individual API operations directly. The key insight is that tool schemas represent the bulk of token usage, so we defer loading them until the LLM explicitly requests them. Progressive search provides complete visibility into available tools through hierarchical navigation. The LLM can explore the tool structure systematically, making it ideal when you need comprehensive coverage or when tool names follow clear patterns. Semantic search trades complete visibility for speed and natural language discovery. It works best when tool descriptions are high-quality and when the LLM's intent can be captured in a natural language query. ## Getting started To enable dynamic toolsets, head to the MCP tab in your Gram dashboard and switch your toolset to either progressive search or semantic search mode. Note that this setting only applies to MCP and won't affect how your toolset is used in the playground, where static tool exposure remains useful for testing and development. ## Important caveats This performance data represents preliminary results from a handful of test runs with unoptimized dynamic implementations. While the trends are clear and consistent, more extensive testing is needed to establish median performance characteristics across different scenarios. The relatively simple tasks used in this benchmarking achieved very high success rates (nearly 100%), but tool call accuracy for more complex workflows requires further validation. Static toolsets with 200 and 400 tools could not be tested due to Claude Code's 200k context window limit. ## What's next Dynamic toolsets are currently experimental as we continue to validate tool selection accuracy across different use cases. We're particularly interested in understanding how they perform with: - Very large toolsets (1,000+ tools) - Complex multi-step workflows - Domain-specific APIs with specialized terminology - Long-running agent sessions We're also exploring hybrid approaches that combine the strengths of both progressive and semantic search, as well as intelligent caching strategies to further optimize token usage for repeated queries. If you're building MCP servers for large APIs, dynamic toolsets make it possible to expose comprehensive functionality without overwhelming the LLM's context window. Try them out and let us know how they work for your use case. ## Additional reading - [Dynamic toolsets documentation](/docs/gram/build-mcp/dynamic-toolsets) - [Code execution with MCP](https://www.anthropic.com/engineering/code-execution-with-mcp) - [Tool design best practices](/mcp/tool-design) # 5-potential-use-cases-for-Arazzo Source: https://speakeasy.com/blog/5-potential-use-cases-for-Arazzo Digital interactions often involve sequences of API calls to achieve goals like user authentication, booking a flight, or ordering a product online. These multi-step workflows rely on passing parameters between various services, with each step dependent on the outcome of preceding API calls. Although API-based workflows are commonplace, they typically aren't formally documented, hindering repeatability and developer experience. Enter [Arazzo](https://github.com/OAI/Arazzo-Specification), a new specification from the [OpenAPI Initiative](https://www.openapis.org/) that can be used to describe an interconnected series of API calls and their dependencies. Announced in [mid-2024](https://youtu.be/EQaGHjMIcD8?si=CxVLfxyLAn7cESM2), Arazzo is on [version 1.0.1](https://github.com/OAI/Arazzo-Specification/pull/318) at the time of writing. Italian for "tapestry," Arazzo is aptly named since it can be used to weave together sequences of API calls to illustrate a specific business pattern. Although new on the scene, the API community is excited about the potential of using Arazzo to standardize deterministic workflows for various use cases. There are countless examples of interlinked API sequences out there, and defining them could greatly boost API-driven development. From better articulating common customer flows to empowering quality engineers and optimizing AI agents, there is a fountain of [possibilities for using Arazzo](https://nordicapis.com/3-example-use-cases-for-arazzo-descriptions/). Below, we'll explore a handful of possible use cases and how they could benefit developer consumers and their end business objectives. ## 1. Making AI more deterministic AI has become a household technology. Yet, large language models (LLMs) are still prone to inaccuracies and hallucinations. Plus, autonomously integrating with APIs and performing interconnected interactions still poses a challenge. This is in part due to a lack of repeatable machine-readable API-related semantics for LLMs. Arazzo could be used to apply more deterministic API processes to AI agents. By ingesting OpenAPI specifications and Arazzo definitions, an AI could understand what operations are available and what workflows they should invoke to perform common actions. This could greatly empower AI agents with greater context, optimize their behaviors, and help reduce errors and randomness. For example, consider an LLM-powered AI assistant within an online food ordering system. Suppose a user asks it to 're-order my last Thai dinner.' An AI could invoke an Arazzo description related to reordering, detailing all the required steps, such as order look-ups, availability and balance checks, and payment processing, to set up and initiate a reorder. ## 2. Simplifying multi-endpoint libraries Have you ever read an OpenAPI definition? The average API has 42 endpoints, and these YAML or JSON files can become pretty unwieldy, with thousands of lines and metadata that are often irrelevant to an individual use case. To make matters more complicated, many workflows call APIs from disparate sources, including internal and external services. Arazzo could be used to greatly abstract complexity for developers by helping to document and generate workflows around common business functions. Rather than fully describing every endpoint and method in an API, Arazzo could help generate [higher-level SDKs](https://speakeasy.hashnode.dev/apis-vs-sdks-why-you-should-always-have-both) that are multi-endpoint and use-case-specific. For instance, consider a developer tooling company that offers recruiting software as a platform. Suppose a common functional use case is to recruit a candidate that matches certain criteria. Well, an Arazzo workflow could document how to search for a user, check they are free for work, initiate outreach in the system, and update the status to `contacted`. It could even automate external calls for background checks or pull in public social media information. Arazzo could deterministically describe the API calls and parameters required to achieve this process. These libraries could even combine interactions across various APIs, greatly streamlining the developer experience. ## 3. Demystifying authorization flows Modern applications don't just authenticate the user — they make sure the user has the correct permissions. Behind the scenes, APIs typically require authorization flows using OpenID Connect and OAuth, involving back-and-forth exchanges between the requesting client, an API gateway, and an external identity server. Arazzo could be used to formulate a sequence of calls for an [OAuth service](https://github.com/OAI/Arazzo-Specification/blob/main/examples/1.0.0/oauth.arazzo.yaml), making a process like [refreshing an access token](https://github.com/OAI/Arazzo-Specification/blob/main/examples/1.0.0/oauth.arazzo.yaml) more transparent and repeatable. For example, the OAI provides an example of using Arazzo to describe a [Financial Grade API (FAPI) profile](https://github.com/OAI/Arazzo-Specification/blob/main/examples/1.0.0/FAPI-PAR.arazzo.yaml), which is a common flow for PSD2 open banking scenarios. Defining this could streamline how developers implement financial security flows, removing some guesswork from the picture. That said, the authentication aspect of OAuth flows are often unspecified and will depend on the exact configurations of the identity server. ## 4. Automating end-to-end API testing The standards for digital experiences are high, meaning quality assurance or site reliability engineers have their work cut out for them. API testing takes this to a whole new level since so much can go wrong with a programmatic interface that is continually updated and versioned. It takes a broad range of routine tests to ensure APIs are stable. From functional testing to performance testing, reliability testing, validation testing, security testing, chaos engineering, linting, and more. QA engineers often create Postman Collections that save API calls, but wouldn't it be nice to automate API testing? Arazzo could greatly aid [end-to-end testing](https://www.speakeasy.com/post/e2e-testing-arazzo) to ensure sequences of API calls are fully functional and meet service-level agreements, bringing efficiency benefits to the testing process. Consider engineers working within a healthcare company — these folks could use Arazzo workflows to automate regulatory compliance checks. For instance, a conformance testing workflow could test whether a system violates regulations around data sharing across regional boundaries when passed certain geographic-specific parameters. ## 5. Standardizing patterns in unified APIs [Unified APIs](https://www.apideck.com/blog/what-is-a-unified-api) take the integration hassle out of aggregating tens, if not hundreds, of software systems and endpoints for similar domains. For instance, take Avage API for construction software, Argyle for payroll services, Duffel for airline booking, or Plaid for integrating with bank data. Many more unified APIs exist for categories like CRM, cloud storage, accounting, and more. Unified APIs could greatly benefit from Arazzo since they already define common user experiences across software domains. There are many common, repeatable pathways within a particular domain. For instance, a unified CRM API could create an agnostic workflow for adding a new qualified lead to a CRM system. Actionable flows for standard processes like this could improve the unified API integration developer experience. ## Optimizing working with Arazzo It's good to note that Arazzo's actual utility will hinge on whether the API tooling ecosystem embraces it. Part of this will be making working with Arazzo more streamlined. Similar to API definition linting tools, the same thing for Arazzo is emerging, enabling you to validate that the Arazzo specification is correct. Speakeasy has open-sourced one such parser for this very purpose, [`speakeasy lint arazzo`](https://www.speakeasy.com/docs/speakeasy-reference/cli/lint/arazzo). Such tools will help API providers and API management platforms integrate Arazzo more easily into their pipelines and offerings. ## Let's see what the builders build By defining common workflows, Arazzo could greatly reduce the mean time for integrations and help standardize complex descriptions typically housed in PDFs or Word documents outside of official API documentation. For developers, it can generate useful onboarding information to create more interactive, "living" workflow documentation. Beyond the examples above, there are countless other [potential use cases](https://nordicapis.com/3-example-use-cases-for-arazzo-descriptions/) for Arazzo specifications. The Arazzo Specification repository also includes use cases such as securing a loan at a [buy now pay later](https://github.com/OAI/Arazzo-Specification/blob/main/examples/1.0.0/bnpl-arazzo.yaml) (BNPL) platform, or [applying coupons to a purchase](https://github.com/OAI/Arazzo-Specification/blob/main/examples/1.0.0/pet-coupons.arazzo.yaml). Arazzo is the first of its kind — a standard with growing industry momentum to denote composite API workflows around specific business goals. From an end consumer perspective, the standard could usher in more predictable [AI agents](https://thenewstack.io/its-time-to-start-preparing-apis-for-the-ai-agent-era/) and better cross-platform customer experiences. For developers, Arazzo could streamline stitching together common request patterns, demystify security flows, and make testing easier. A lot is hypothetical now, but the future is looking bright for this new standard. Now, it's just up to the builders to build. # a-copilot-for-your-api-spec-and-pagination Source: https://speakeasy.com/blog/a-copilot-for-your-api-spec-and-pagination May has been the most heads down month till date ! All of us at Speakeasy have been busy onboarding customers and gearing up for a few exciting announcements coming up (stay tuned 🚀!). To that end we've got some exciting feature releases from this month all in the theme making maintenance and usage of your complex API simpler than ever. On an unrelated note, Speakeasy is growing rapidly and if you'd like to learn more about our engineering, marketing and sales roles please feel free to email back or check out our jobs page! ## New Features LLM powered spec maintenance:: Go further with speakeasy validations with suggestions that suggest actionable changes to your API spec powered by ChatGPT. Maintaining an API spec is hard work and error prone. As APIs and the organizations they support get larger your API spec becomes an important artifact that deserves its own maintenance workflow. Our goal is to ease that burden with a simple speakeasy suggest command. Our LLM powered API will suggest changes to your spec in service of making your API better documented and having more ergonomic SDKs. Soon to be available through our Github pull request workflow - one click merge changes around the corner! ![gif of a speakeasy suggest command](https://storage.googleapis.com/speakeasy-design-assets/emails/changelog26/ezgif-1-263696a906.gif) ## Auto Pagination Speakeasy Managed SDKs now support the ability to generate SDKs that have your pagination rules built-in. Adding pagination into your SDK can reduce the burden on your users by removing the need to manually manage multiple pages of responses. If your API is paginated we will provide an iterable object that users can use to loop over. For end users this means an experience that resembles: ```python response = sdk.paginatedEndpoint(page=1) while response is not None: # handle response response = response.next() ``` Our SDKs, starting with python, go and typescript come with support for `cursor` and `offsetLimit` pagination. This is enabled per-operation using the `x-speakeasy-pagination` extension. Getting pagination added to your SDK is as easy as adding a configurable extension to your spec. ```yaml x-speakeasy-pagination: type: offsetLimit inputs: - name: page in: parameters type: page outputs: results: $.resultArray ``` More on that [here](/docs/customize-sdks/pagination/) ! ## Improvements and Bug Fixes - Managed SDKs: - All languages: Support for deprecations. Get strike through IDE hints for deprecated operations - Terraform: Support for byte arrays and additional properties - Speakeasy CLI - Now supports an interactive mode ! - Support for chocolatey package manager for windows # a-dev-portal-on-demand Source: https://speakeasy.com/blog/a-dev-portal-on-demand ## New Features - **EasyShares** - Building a robust developer portal takes time. Don't wait months to get the benefits. Install the Speakeasy SDK, and autogenerate your client-specific dev portal in minutes. Make it easy for your API users to troubleshoot errors, and save your developers time that would otherwise be spent on zoom meetings. With a couple clicks you have a unique site that can be shared with your customer, with full control over the data. Set the site to expire after an hour, a day, a week, however long it takes your customer to finish the task at hand! [Watch Alexa walkthrough the new feature!](https://www.loom.com/share/17bd5adcd2ab44a6bb91a7d4e0d6abbb) ## Incremental Improvements & Fixes - **\[Bug Fix\] Scaled Schema Differ** - Those with large APIs no longer need worry! The OpenAPI schema differ now works with obscenely large OpenAPI schemas, so you can see changes to your API no matter its size. - \[**Improvement**\] **Auto-reload on filter changes** - The usage Dashboards now auto-refresh when you change your filters. # a-monorepo-for-your-sdks Source: https://speakeasy.com/blog/a-monorepo-for-your-sdks We're living in the age of the monorepo. And SDKs are no different. The SDK monorepo sturcture was popularized by Amazon, but it's become an increasingly common pattern for companies that are offering multiple APIs. The main benefit of providing SDKs in a monorepo is that it can help users with service discovery, with the downside that the SDKs are much larger in size. Ultimately it's a choice of personal preference. But whether you want your SDKs to adhere to the more traditional “repo per API” or you decide to adopt the monorepo, Speakeasy can create SDKs that your users will love, in the structure you prefer. ## New Features **Support for MonoRepo Structure:** Centralize your APIs' SDKs in one repository to make service discovery easy for your users. Although services are grouped together, individual API updates and publishing can still be managed granularly, so your team maintains the same shipping velocity. See an example of a monorepo below: **Manage API Keys Without a Gateway**: Our self-serve API key solution is built to easily plug into any major gateway provider and help you externalize your API key management. However, a gateway doesn't always make sense for company that's just starting their API journey. ## Improvements **Improved Usage Snippets**: People are going to copy/paste example code, so we've made sure the generated examples in Readme's are accurate and compilable. # add-prompts-to-your-mcp-server Source: https://speakeasy.com/blog/add-prompts-to-your-mcp-server MCP is much more than an "AI-native HTTP wrapper". In addition to defining available API endpoints and operations, MCP allows you to define prompts for AI clients to use. These prompts serve as templates that AI uses to execute workflows. Speakeasy's MCP server generator now allows you to bundle prompts with your server, combining the efficiency of a generated server with the full functionality offered by the protocol. ## How it works When you define prompts, your generated MCP server automatically exposes `prompts/list` and `prompts/get` endpoints. These endpoints allow MCP clients to discover (`list`) and execute (`get`) prompts. Users of the MCP client can then discover and execute these prompts directly. The use cases for prompts are extensive. Common examples include: - Chaining multiple API interactions - Guiding specific workflows - Including context from outside the API ## Getting started Adding prompts to your server is straightforward. Define prompts in your MCP server's `/custom/customPrompts.ts` file as objects with a name, description, and prompt function. In the description, provide instructions for the MCP client to execute, including the use of tools in your MCP server. ```typescript filename="/custom/customPrompts.ts" import { z } from "zod"; import { formatResult, PromptDefinition } from "../prompts.js"; // Define arguments for the link shortening workflow const shortenLinksArgs = { urls: z.array(z.string()).describe("List of URLs to shorten"), domain: z.string().optional().describe("Optional custom domain to use"), }; export const prompt$shortenAndTrackMarketingLinks: PromptDefinition< typeof shortenLinksArgs > = { name: "shorten-and-track-links", description: "Shorten multiple URLs and set up analytics tracking", args: shortenLinksArgs, prompt: (client, args, _extra) => ({ messages: [ { role: "user", content: { type: "text", text: ` I need to create shortened links for a marketing campaign and track their performance. Please help me: 1. Create shortened links for each of these URLs: ${args.urls.join(", ")} ${args.domain ? `2. Use our custom domain: ${args.domain}` : "2. Use the default domain"} 3. Add UTM parameters for campaign tracking 4. Set up analytics to monitor clicks 5. Create a summary report of all the links Please use the available tools to complete these tasks efficiently. `, }, }, ], }), }; ``` Then register the prompt in the `server.extensions.ts` file: ```typescript import { prompt$shortenAndTrackMarketingLinks } from "./custom/customPrompts.js"; import { Register } from "./extensions.js"; export function registerMCPExtensions(register: Register): void { register.prompt(prompt$shortenAndTrackMarketingLinks); } ``` # Simulated agent request Source: https://speakeasy.com/blog/agent-experience-introduction import { Screenshot } from '@/components/ui/screenshot'; A [recent Gartner survey reveals 85% of support teams considering conversational AI in 2025](https://www.gartner.com/en/newsroom/press-releases/2024-12-09-gartner-survey-reveals-85-percent-of-customer-service-leaders-will-explore-or-pilot-customer-facing-conversational-genai-in-2025). As AI agents become more integral to business operations, designing software for human users and developers is no longer enough. First we optimized for user experience (UX), hiding complexity behind elegant interfaces, ergonomic flows, and delightful visuals. Then we learned to care about developer experience (DX), prioritizing tooling, documentation, APIs, and SDKs that make integration frictionless. Today, AI agents are no longer hidden tools. They answer support requests, act on user input, trigger system workflows, and can function autonomously on tasks for days. But unlike humans, agents can't improvise. To function without human intervention, agents need clear tools, documentation, permissions, and fallback paths. This is agent experience (AX), a concept [introduced in early 2025 by Mathias Biilmann, CEO of Netlify](https://biilmann.blog/articles/introducing-ax/). AX involves designing systems, tools, and workflows so agents can operate reliably and autonomously. In practice, this means making tools predictable, well-documented, appropriately scoped, and recoverable, with proper error handling so agents know exactly what to do in any situation. This post unpacks the core principles of AX, how they relate to UX and DX, and practical implementation strategies. ## AX at the intersection of UX and DX Agent experience (AX) bridges [user experience (UX)](https://www.pencilandpaper.io/articles/ux-design-documentation-guide) and [developer experience (DX)](https://www.commonroom.io/resources/ultimate-guide-to-developer-experience/) because agents may need to understand and serve users, interface reliably with APIs and tools, or both. So AX principles fall into two categories: User-facing principles for agents that serve humans and system-facing principles for agents that integrate with APIs, databases, and other infrastructure. Many agents need both. ### The UX layer: Designing for human expectations When agents interact with users, AX must prioritize human-centered design principles. - **Predictability:** Users need to trust that agents will behave consistently every time. If your agent can create a Trello card from a specific instruction, it should succeed whether it's asked once or a hundred times. - **Transparency:** Users should know they are interacting with an agent, not a human. Agents should introduce themselves and clearly state their capabilities. This avoids confusion and unrealistic expectations. - **Coherence:** Agents must follow the natural flow of tasks and match user expectations. For example, booking a flight should include a confirmation email. Completing a booking without following up leaves users uncertain and frustrated. ### The DX layer: Building for agent autonomy While user-facing AX principles focus on interaction design and user flows, system-facing principles are about infrastructure, tooling, and system architecture. - **Toolability:** Expose only the tools agents actually need, with clear documentation and proper scoping. Don't overwhelm a sales-focused agent with 300 operations across all departments — scope tools by domain and restrict access to relevant endpoints only. - **Recoverability:** Agents must handle failures gracefully with retry logic, alternate paths, and user communication. For example, "That card didn't go through. Want to try another one or switch to PayPal?" - **Traceability:** Track what agents do, when, and why using headers like `X-Agent-Request: true` and `X-Agent-Name`. Without this, debugging becomes guesswork, and you can't distinguish agent actions from human actions in logs. ## Putting AX principles into practice Implementing AX comes down to clear, actionable practices you can apply to your development workflow, tooling, and API design. Let's see how this works in practice. ### Design prompts for reliable agent behavior Prompts define how agents behave and are critical to their autonomy. The best tools, APIs, and documentation won't save your product if the prompt doesn't define the right intent, scope, or fallback behavior. Let's say you're building a custom service agent. Here's an example of a prompt that won't work: ```markdown You are a customer assistant. Help users get refunds by checking the status on `/orders` and `/refunds`. ``` This prompt fails because: - **It doesn't define scope:** The agent doesn't know which APIs or tools it can use. - **It doesn't specify criteria:** There are no rules to decide when a refund is allowed. - **It doesn't provide format:** The agent has no guidance on how to structure its responses. - **It doesn't include fallback:** There's no plan for what to do if the refund attempt fails. Let's revise the prompt: ```markdown You are a refund-processing agent for an e-commerce site. When a user asks for a refund: 1. Greet the customer. 2. Ask them for their order ID and confirm it matches the format: `ord_XXXXXX`. 3. Call the `/orders/{id}` endpoint to check if the order exists and is in `delivered` status. 4. If eligible, use the `/refunds` endpoint with the provided amount and reason. 5. If the refund fails (e.g. 403 or 500), respond with: “I couldn't process that refund. Please contact support or try again later.” ``` This revised prompt works because: - **Scope is defined:** The agent is limited to using the `/orders` and `/refunds` endpoints. - **Criteria are clear:** The agent only proceeds if the order status is `delivered`. - **Format is enforced:** The agent knows what steps to follow and what output to generate. - **Fallback is handled:** The agent informs the user if the refund fails and offers an alternate path. ### Create documentation for LLMs, not humans While a human can infer context from a sentence or connect steps based on intuition, agents need clear instructions about what to call, when, and in what order. While this might result in more verbose documentation, it's necessary to enable autonomy. Take this example. Your API requires an address to be set on a cart before validating it. You've shown this flow in a tutorial for developers, which is enough for a human to follow. But your agent reads only the endpoint descriptions. Here's what the current description looks like: ```yaml paths: /cart/{cartId}/billing-address: put: summary: Set the billing address for the cart. description: | This endpoint assigns a billing address to the given cart. parameters: - name: cartId in: path required: true schema: type: string requestBody: required: true content: application/json: schema: $ref: '#/components/schemas/Address' responses: '200': description: Billing address successfully added. '400': description: Invalid address payload. ``` This description is not useful for agents as it doesn't provide the necessary context. - When should this endpoint be called in the workflow? - What happens if the cart already has a billing address? - What format requirements must be met to avoid errors? Agent-focused documentation should look like this: ```yaml paths: /cart/{cartId}/billing-address: put: summary: Set the billing address for the cart. description: | This endpoint assigns a billing address to the given cart. It must be called before the cart can proceed to checkout, as a valid billing address is required for order validation and payment processing. Agents: - Use this operation after the cart has been created but before checkout validation. - If the cart already has a billing address, this call will overwrite it. - Ensure the address format complies with the schema to avoid validation errors. parameters: - name: cartId in: path required: true schema: type: string requestBody: required: true content: application/json: schema: $ref: '#/components/schemas/Address' responses: '200': description: Billing address successfully added. '400': description: Invalid address payload. ``` This extended documentation helps agents choose the right endpoints for their tasks. However, adding agent-specific descriptions to an OpenAPI document will result in agent-focused text appearing in documentation for humans when rendered by tools like Swagger UI and Redoc. The `x-speakeasy-mcp` extension in [Speakeasy's MCP server generation](/docs/model-context-protocol) addresses this challenge directly. The extension lets you include agent-specific operation names, detailed workflow descriptions, and scopes directly in your OpenAPI document. Since documentation renderers like Swagger UI don't recognize custom extensions, they ignore this agent-focused content. Your human-readable documentation stays clean while agents get the detailed context they need. ```yaml paths: /cart/{cartId}/billing-address: put: operationId: setBillingAddress tags: "AI & MCP" summary: Set billing address for cart description: API endpoint for assigning a billing address to a cart before checkout. x-speakeasy-mcp: disabled: false name: set-billing-address scopes: [cart, write] description: | Adds or updates the billing address associated with the cart. - Use this operation after the cart has been created. - Ensure this step is completed before attempting to validate or checkout the cart. - Provide all mandatory address fields to avoid validation failures. - If the billing address already exists, this call will replace it. - This operation has no side effects besides updating the billing address. parameters: - name: cartId in: path required: true schema: type: string requestBody: required: true content: application/json: schema: $ref: '#/components/schemas/Address' responses: '200': description: Billing address successfully added. '400': description: Invalid address payload. ``` ### Allow agents to build SDKs and manage API keys Just as API documentation must be tailored for agents to act autonomously, so must SDKs. The quality of the SDK matters, including the defined types, the `README.md`, and the docstrings on the methods, classes, or objects. Agents may be designed to write or complete their own SDKs when needed. In this context, the best SDK for an agent is often the API documentation itself. A well-crafted and precise OpenAPI document is often more valuable to an agent than any wrapper code, because it offers direct, structured access to the service. For APIs that require authentication, agents need clear instructions on how to handle secured requests, specifically how to: - Create new API keys or tokens for authentication. - Refresh tokens or keys when they expire. - Handle authentication failures gracefully and know when to retry or escalate. Without this level of clarity, an agent cannot reliably access or interact with secured APIs. ### Control access with tool curation and role scopes Let's say you've built an e-commerce agent to help customers search for products and add them to their cart. The agent should not be able to request credit card details or complete the checkout. Agents' capabilities must be scoped tightly to their intended purpose. Because agents can perform powerful and wide-ranging actions, security is a core pillar of AX. Poorly designed access controls can lead to data leaks, unintended behavior, or even dangerous operations. Following the AX principle of toolability, the most effective approach to minimizing these risks is through the tools you expose to the agent. For example, to restrict an agent to product search, expose just read-only product operations in the OpenAPI document. You can curate tools manually or automatically. - **Manual curation** involves directly editing an OpenAPI document to remove unwanted operations. - **Automated curation** uses automated tooling to generate a curated document based on predefined scopes. Speakeasy supports [automated tool curation](/docs/model-context-protocol#customizing-mcp-tools) using the `x-speakeasy-mcp` extension, which lets you define scopes that the MCP server generator understands and uses to generate tools accordingly. For example, you can define scopes (like `read` and `products`) in your full OpenAPI document, and the Speakeasy MCP server generator will automatically create a curated toolset that includes only the operations matching your specified scopes. ```yaml paths: /products: get: operationId: listProducts tags: [products] summary: List products description: API endpoint for retrieving a list of products from the CMS x-speakeasy-mcp: disabled: false name: list-products scopes: [read, products] description: | Retrieves a list of available products from the CMS. Supports filtering and pagination if configured. This endpoint is read-only and does not modify any data. ``` When you start the server in read-only mode, only read operations will be made available as tools. ```json { "mcpServers": { "MyAPI": { "command": "npx", "args": ["your-npm-package@latest", "start"] "env": { "API_TOKEN": "your-api-token-here" } } } } ``` You can further scope operations by specifying additional scopes like `products` if you need to. Tool curation controls what agents can see, but you also need to control what they can do with those tools. Use role-based access control to limit agents to only the permissions they need. In our e-commerce example, you can limit agent permissions in a system like this: ```python ROLE_PERMISSIONS = { "agent": {"search_products", "add_to_cart"}, "admin": {"search_products", "add_to_cart", "checkout", "manage_users"}, "user": {"search_products", "add_to_cart", "checkout"} } class Agent: def __init__(self, role: str): self.role = role self.permissions = ROLE_PERMISSIONS.get(role, set()) def can(self, action: str) -> bool: return action in self.permissions ``` ### Log and audit every agent action Logging and auditing are essential to AX design. When agents make mistakes, you need to distinguish their actions from actions taken by humans. Include metadata fields or custom headers in every agent-generated request to clearly flag the source. ```python import requests # Example request made by an AI agent headers = { "Content-Type": "application/json", "X-Agent-Request": "true", # Custom header to flag the request as agent-initiated "X-Agent-Name": "product-recommender-v1" # Include the agent name or version } payload = { "query": "search term", "filters": ["in-stock", "category:electronics"] } response = requests.post("https://api.example.com/search", json=payload, headers=headers) if response.ok: print("Agent request succeeded:", response.json()) else: print("Request failed with status:", response.status_code) ``` AI observability and monitoring platforms like [Langfuse](https://langfuse.com/), [Helicone](https://www.helicone.ai/), and [LangSmith](https://www.langchain.com/langsmith) help you track agent behavior, catch errors early, and address issues quickly. ## Five steps to better AX As AI agents move from novelty to infrastructure, AX determines whether they deliver reliable automation or frustrated users. To start implementing great AX without complicating things: 1. **Write endpoint docs for agents.** Explain when to call, what it does, and what follows. 2. **Scope tools tightly.** Don't expose your entire API, just what the agent truly needs. 3. **Include tracing headers** like `X-Agent-Request: true` to trace and debug agent actions. 4. **Design prompts with clear goals, inputs, validation steps, and fallbacks.** 5. **Build for failure.** Retries, alternatives, and user-facing messages must be part of the flow. # announcing-easysdk-generator Source: https://speakeasy.com/blog/announcing-easysdk-generator ## What We've Built We're excited to announce that we are publicly launching an [SDK generator](#) that improves upon the [OpenAPI service](https://github.com/OpenAPITools/openapi-generator). In our view the biggest problem with the OpenAPI generator was that it produced client libraries that were untyped and unopinionated. That's why last week we focused on building a generator that is able to handle typing correctly and is opinionated about what makes for a good SDK: - **Low-dependency** - To try and keep the SDK isomorphic (i.e. available both for Browsers and Node.JS servers), we wrap axios, but that's it.This is intended to be idiomatic typescript; very similar to code a human would write; with the caveat that the typing is only as strict as the OpenAPI specification. - **Static Typing** - At this point static typing is everywhere. So wherever possible, we generate typed structures, construct path variables automatically, pass through query parameters, and expose strictly typed input / output body types. - **Language idiomatic & opinionated** - There's value in being neutral, but we felt like there is more value in being opinionated. We've made choices for how things like Pagination, Retries (Backoff/Jitter etc), Auth integrations, should be handled in the SDK. ## Why We Think SDKs Are Important A good developer experience means flattening the learning curve by meeting developers where they already; that's why every company with an [API platform](/post/why-an-api-platform-is-important/) should strive to offer SDKs for integrating with their APIs. Language-idiomatic SDKs improve user productivity by removing the need for writing the boilerplate required to send API requests and parse response objects. Companies that offer SDKs get faster adoption, spend less time troubleshooting, and provide an overall better developer experience. We look forward to hearing from the community what they think of the service. We'd love to know what else people would want to see included in an SDK, and what languages we should support next. # mTLS via curl: Should you be looking to grant unscoped access to a specific API Route to trusted consumers, Source: https://speakeasy.com/blog/api-auth-guide ## API Authentication Overview If you're selling an API Product, one of the first decisions you're going to have to make is how to authenticate your users. This decision, once made, is hard to go back upon; any significant change will require user action to keep their integration working. This blog post is intended to be a guide to the different API Authentication methods in common use, and the tradeoffs to consider between them, generally categorized into the following 3 metrics. * **Time to API Call** * **Ease of API Producer Implementation** * **Ease of API Consumer Integration** This post is inspired by work we recently undertook at Speakeasy (API DevEx tooling company) to build an API Key authorization flow that integrates into any API Gateway, and allows users to self-service and rotate their keys. For this project we evaluated all the standard approaches to authentication before deciding on a novel approach: signed tokens as API Keys, but with 1 signing key per API key. What follows are a deep dive on the typical methods, as well as our approach. ## Types of Authentication Authenticated APIs require some way of identifying the client making the request, so that the API provider can authorize the requests to be processed, and identify the subset of data that can be accessed. From an API Consumer perspective, the flow is usually: 1. You get a **Secret Key** from the service (e.g. in an authenticated Web UI). 2. You store that **Secret Key** somewhere securely, such that your application can access it. 3. You use that **Secret Key** within your application logic to authenticate with the API. This is incredibly simple, and hence has great Developer Experience (DX). However, from an API Producer Perspective, it's not so simple. There are choices you need to make about how the **Secret Key** is implemented which greatly impacts the Security Model of your application. Once you have users in production, Machine to Machine (M2M) authentication is hard to change, assuming you don't want to break existing integrated users. Therefore, choose wisely: 1. **Opaque Token / Shared Secrets** 2. **Public / Private Key Pairs** 3. **OAuth 2.0 Secrets** 4. **Signed Tokens (one Signing Key)** 5. **Signed Tokens (many Signing Keys)** Let's look at the advantages and disadvantages of each approach… ### Opaque Tokens / Shared Secrets An _Opaque Token_ is a _Shared Secret_ that is used to authenticate a client. It is _Opaque_ in that there is no message to read: all you can do with it is look up its existence in a centralised store (e.g. a database), and then, if it exists, you know who the user is. This is functionally the same as a password, except that it is ideally generated by a process which ensures the entropy of the token is high enough that it is entirely unguessable. Assuming this token is passed into the API in the `Authorization` header, from an API Consumer perspective, accessing the API is as simple as: ```sh curl https://api.example.com/v1/endpoint --header "Authorization ${API_KEY}" ``` From an API Producer perspective, there's a few more moving parts, but it's usually pretty trivial to implement: ```mermaid sequenceDiagram participant API Consumer[Bob] participant API participant Authorization Server rect rgba(0, 0, 0, 0.05) note over API Consumer[Bob],Authorization Server: Configure Key API Consumer[Bob]-->>API: AUTHORIZED SESSION [Bob] with [API] API-->>API Consumer[Bob]: API Consumer[Bob]->>API: Request New Secret Key Note right of API: Generate some random bytes.
Render bytes as [Secret Key] API->>Authorization Server: Store [Bob, [Secret Key]] Authorization Server-->>API: Success API-->>API Consumer[Bob]: [Secret Key] end rect rgba(0, 0, 0, 0.05) note over API Consumer[Bob],Authorization Server: Authorized API Request API Consumer[Bob]->>API: [Req] with [Secret Key] API->>Authorization Server: Who is this? [Secret Key] Authorization Server-->>API: Bob Note right of API: Process Request under context Bob API-->>API Consumer[Bob]: [Resp] end ``` * **Time to API Call**: As Fast as it gets. * **Ease of API Consumer Integration**: Very Easy * **Ease of API Producer Implementation**: Very Easy * **Other Considerations**: * Often Difficult to integrate into an API Gateway * Any validation requires a lookup where-ever these are stored. If you solely store them in the DB, this means that all lookups require a DB Read, which can cause additional latency to every request. ### OAuth 2.0 Secrets ```mermaid sequenceDiagram participant API Consumer[Bob] participant API participant Authorization Server rect rgba(0, 0, 0, 0.05) note over API Consumer[Bob],Authorization Server: Configure Key API Consumer[Bob]-->>API: AUTHORIZED SESSION [Bob] with [API] API-->>API Consumer[Bob]: API Consumer[Bob]->>API: Request New oAuth Application Note left of API: Generate some random bytes.
Render bytes as [Client ID], [Client Secret]
(AKA Secret Key). API->>Authorization Server: [Bob, [Secret Key]] Authorization Server-->>API: Success API-->>API Consumer[Bob]: [Secret Key] end rect rgba(0, 0, 0, 0.05) note over API Consumer[Bob],Authorization Server: Authorized API Request API Consumer[Bob]-->>Authorization Server: SECURE SESSION [Unknown] with [Authorization Server] Authorization Server-->>API Consumer[Bob]: API Consumer[Bob]->>Authorization Server: GET /oauth/token with [Secret Key] Note left of Authorization Server: Lookup [Secret Key] as Bob
Generate short-lived JWT with {"sub":"Bob"} claims Authorization Server-->>API Consumer[Bob]: JWT["Bob"] API Consumer[Bob]-->>API: SECURE SESSION [Unknown] with [API] API-->>API Consumer[Bob]: API Consumer[Bob]->>API: [Req] with header "Authorization Bearer \${JWT["Bob"]}" Note left of API: Process Request under context Bob API-->>API Consumer[Bob]: [Resp] end ``` OAuth is commonly associated with user authentication through a social login. This doesn't make sense for API applications, as the system authenticates and authorizes an application rather than a user. However, through the Client Credentials Flow ([OAuth 2.0 RFC 6749, section 4.4](https://tools.ietf.org/html/rfc6749#section-4.4])), a user application can exchange a **Client ID**, and **Client Secret** for a short lived Access Token. In this scenario, the **Client ID** and **Client Secret** pair are the **Shared Secret** which would be passed to the integrating developer to configure in their application. In the OpenID Connect (OIDC) protocol, this happens by making a request to the **/oauth/token** endpoint. ```sh TOKEN=$(curl --header "Content-Type: application/x-www-form-urlencoded" \ --request POST \ --data "grant_type=client_credentials" \ --data "client_id=CLIENT_ID" \ --data "client_secret=CLIENT_SECRET" \ https://auth.example.com/oauth/token | jq -r '.access_token') curl --header "Authorization Bearer $TOKEN" \ https://api.example.com/v1/endpoint ``` * **Time to API Call**: Slow. * **Ease of API Consumer Integration**: Difficult * **Ease of API Producer Implementation**: Difficult * **Other Considerations**: * Can enable additional use-cases, such as granting third party systems the capability to make API calls on behalf of your users. ### Public/Private Key Pairs A Public Private Key Pair allows for a user to hold a secret and the server to validate that the user holds a secret, without the server ever holding the secret. For the purpose of authenticating users, this mechanism has the lowest attack surface : i.e. it is the most secure. ```mermaid sequenceDiagram participant API Consumer[Bob] participant API participant Authorization Server rect rgba(0, 0, 0, 0.05) note over API Consumer[Bob],Authorization Server: Configure Key API Consumer[Bob]-->>API: AUTHORIZED SESSION [Bob] with [API] API-->>API Consumer[Bob]: Note right of API Consumer[Bob]: Generate Public/Private Key Pair API Consumer[Bob]->>API: Send Public Key API->>Authorization Server: Store [Bob, [Public Key]] Authorization Server-->>API: Success API-->>API Consumer[Bob]: Success end rect rgba(0, 0, 0, 0.05) note over API Consumer[Bob],Authorization Server: Authorized API Request API Consumer[Bob]-->>API: SECURE SESSION [Unknown] with [API] API-->>API Consumer[Bob]: API Consumer[Bob]->>Authorization Server: Hello, I am Bob Note left of Authorization Server: Generate Random Number [NONCE] Authorization Server-->>API Consumer[Bob]: Hello Bob, please sign [NONCE] Note right of API Consumer[Bob]: Sign [NONCE] with Private Key as [PROOF] API Consumer[Bob]->>Authorization Server: [PROOF] Note left of Authorization Server: Validate [NONCE] signature using [Public Key[Bob]] Authorization Server-->>API Consumer[Bob]: Success. [Session Token] API Consumer[Bob]-->>API: AUTHORIZED SESSION [Bob] with [API] via [Session Token] API-->>API Consumer[Bob]: end ``` The cost and complexity of building and maintaining a Public/Private Key Authentication mechanism, without exposing the Private Key, opening up replay attacks, or making a mistake in implementation somewhere can be high. If you're selling an API to multiple consumers, it's unlikely that it will be as trivial as the following `curl` to invoke the API that you want; as the integrating system will need to understand the protocol you choose. There are also complexities regarding the certificate lifecycle, and the need to either manage certificate rotation, or pin (hardcode) each certificate into the system. ```sh # this can usually be configured by some API Gateway products in infrastructure configuration. curl --cacert ca.crt \ --key client.key \ --cert client.crt \ https://api.example.com/v1/endpoint ``` * **Time to API Call**: Slow. * **Ease of API Consumer Integration**: Difficult * **Ease of API Producer Implementation**: Difficult * **Other Considerations**: * Severity of the public key being leaked is functionally zero -- no intermediary system holds enough data to make/replay requests except the original sender. ### Signed Tokens as API Keys A Signed Token is secret; in the same way that a Shared Secret is secret. However, due to standardization of technologies, it is starting to become commonplace to use long-lived Signed Tokens, in the form of JWTs (JSON Web Tokens), as API Keys. This enables the following pattern: ```sh curl --header "Authorization Bearer ${API_KEY}"\ http://api.example.com/v1/endpoint ``` ```mermaid sequenceDiagram participant API Consumer[Bob] participant API participant Authorization Server rect rgba(0, 0, 0, 0.05) note over API Consumer[Bob],Authorization Server: Configure Key API Consumer[Bob]-->>API: AUTHORIZED SESSION [Bob] with [API] API-->>API Consumer[Bob]: API Consumer[Bob]->>Authorization Server: Request New Secret Key Note left of Authorization Server: Make JWT[Secret Key] with Signing Key [K] via:
SIGN({"sub": Bob, "exp": 3 months}, K). Authorization Server-->>API Consumer[Bob]: [Secret Key] end rect rgba(0, 0, 0, 0.05) note over API Consumer[Bob],Authorization Server: Authorized API Request API Consumer[Bob]-->>API: SECURE SESSION [Unknown] with [API] API-->>API Consumer[Bob]: API Consumer[Bob]->>API: [Req] with header "Authorization Bearer \${JWT["Bob"]} API->>Authorization Server: Check JWT["Bob"] Note left of Authorization Server: Validate JWT[Secret Key] with Signing Key [K] Authorization Server-->>API: Success Note left of API: Process Request under context Bob API-->>API Consumer[Bob]: [Resp] end ``` * **Time to API Call**: Fast. * **Ease of API Consumer Integration**: Simple * **Ease of API Producer Implementation**: Simple * **Other Considerations**: * Difficult to revoke tokens: either a whitelist/blacklist is used (in which case, little advantage exists over shared secrets), or the signing key must be rotated. * Easy to add complexity through custom claims into the token, which can lead to complexity migrating tokens into a new form. * Easy to block requests from reaching application servers through an API Gateway and Asymmetric Keys (split into Private JWTs and Public JWKs). ### Our Approach: Signed Tokens as API Keys, but 1-Signing-Key-Per-API-key There are 3 problems with the Signed Tokens as API Keys pattern: 1. Revoking a key is hard: it is very difficult for users to revoke their own keys on an internal compromise. 2. Any compromise of the Signing Key is a compromise of all keys. 3. API Gateways can't usually hook into a whitelist/blacklist of tokens. To tackle these, we can do three things: 1. Use Asymmetrically Signed JWTs, storing and exposing a set of Public Keys via a JWKS (JSON Web Key Set) URI. 2. Sign each Token with a different Signing Key; burn the Signing Key immediately afterwards. 3. Ensure that API Gateways only retain a short-lived cache of JWKS (the Public Key to each Signing Key). ```mermaid sequenceDiagram participant API Consumer[Bob] participant Authorization Server participant API rect rgba(0, 0, 0, 0.05) note over API Consumer[Bob],API: Configure Key API Consumer[Bob]-->>Authorization Server: AUTHORIZED SESSION [Bob] with [Authorization Server] Authorization Server-->>API Consumer[Bob]: API Consumer[Bob]->>Authorization Server: Request New Secret Key [K] Note left of Authorization Server: Create a new Signing Key Pair [K]
(e.g. EdDSA Certificate with High Entropy)
Sign [Bob] with [K["PrivateKey"]] as [Secret Key]
Store [Bob, K["PublicKey"]]
Burn K["Private Key"] Authorization Server-->>API Consumer[Bob]: [Secret Key] end rect rgba(0, 0, 0, 0.05) note over API Consumer[Bob],API: Authorized API Request API Consumer[Bob]-->>Authorization Server: SECURE SESSION [Unknown] with [Authorization Server] Authorization Server-->>API Consumer[Bob]: API Consumer[Bob]->>Authorization Server: [Req] with header "Authorization Bearer \${JWT["Bob"]}" Note left of Authorization Server: Decode["Secret Key"] into JWT
Lookup Bob["Public Key"] via JWT.KID
Validate ["Secret Key"] matches Public Key["KID"] Authorization Server->>API: [Req], [Bob] Note left of API: Process Request under context Bob API-->>Authorization Server: [Resp] Authorization Server-->>API Consumer[Bob]: [Resp] end ``` This gives us a flow that's very similar to a shared secret, but with the advantage that: * Revoking a key is easy: the Public Key just needs to be removed from the JWKS and caches invalidated. * There is no Signing Key to compromise ; all Authorization Server state can be public. With the tradeoff: * Any API Gateway that validates a JWT must be able to regularly fetch JWKS (and cache them) from the Authorization Server. * After creating/revoking a key, there will be a short time delay (we generally configure this to be 15 seconds) whilst the key propagates into all API Gateway JWK caches. * More compute is required when constructing the Initial API Key; due to the generation of a public/private key pair. * You need to give the API Gateway application cluster slightly more memory to keep all Public Keys (1 per API key) in memory. #### In Practice: Envoy / Google Endpoints / EspV2 Envoy is a proxy server that is used to route traffic to a backend service. We ran extensive tests with ESPV2 (Envoy Service Proxy V2) and Google Endpoints, and found/validated the following performance characteristics as the number of keys increased. ```yaml # Espv2 is configured with an OpenAPI specification to use an external JWKs URI to validate incoming API Keys securityDefinitions: speakeasy_api_key: authorizationUrl: "" flow: "implicit" type: "oauth2" x-google-issuer: "https://app.speakeasy.com/v1/auth/oauth/{your-speakeasy-workspace-id}" x-google-jwks_uri: "https://app.speakeasy.com/v1/auth/oauth/{your-speakeasy-workspace-id}/.well-known/jwks.json" x-google-audiences: "acme-company" ``` We ran benchmarks of up to 1138688 API keys, structured as public/private 2048-bit RSA keypairs with a single RS256 Signed JWT-per-JWK, using a 8GiB Cloud Run managed Espv2 instance. At this key size and at ~100 requests/second, we observed a peak of ~37% utilization of memory, with ~7% utilized at no API keys. This implies an 8GiB EspV2 instance should scale to ~3M API Keys at this request rate. This also implies that this mechanism will scale with Envoy RAM with a minor deviation in maximum latency, to the degree of ~3.5ms additional maximum request latency every 8192 API keys. The average additional latency introduced by large numbers of API keys is affected to a much lesser degree, ~0.2ms every 8192 API Keys. ![envoy-scaling.png](/assets/blog/api-auth-guide/envoy-scaling.png) Given most API Applications have less than 3M API Keys active at any given time, this approach, in our belief, combines the best of both worlds: Public/Private Key Crypto, with the ease of an API Key. # api-change-detection-open-enums Source: https://speakeasy.com/blog/api-change-detection-open-enums import { Callout, ReactPlayer } from "@/lib/mdx/components"; Two exciting new features are coming to Speakeasy this week: API Change Detection and Open Enum support. Let's get into it 👇
## API Change Detection For as long as there have been APIs, the dreaded "unintended breaking change" has tortured API developers everywhere. Countless hours spent debugging, untold numbers of damage control meetings with unhappy customers, the inevitable "who broke the API?" finger-pointing; it's not pretty. And yet decades later, it still feels like it's needlessly difficult for organizations to get good observability into how their API is changing. This is why we're so excited to be tackling this problem head-on. Today's release of API change detection is just the beginning. Whenever there's a change to your OpenAPI document, Speakeasy will automatically detect it and notify you. The changes will be summarized directly in your pull request, with a callout for when changes are breaking. If you want to dive into the details, head to our dashboard for the full breakdown. --- ```typescript import { SDK } from "@acme/cms"; import { Unrecognized } from "@acme/cms/types"; const cms = new SDK(); const post = await cms.posts.get("open-enums"); let icon: "📸" | "🎨" | "🏈" | "❓"; switch (post.category) { case "lifestyle": icon = "🎨"; break; case "photography": icon = "📸"; break; case "sports": icon = "🏈"; break; default: post.category satisfies Unrecognized; icon = "❓"; break; } ``` ## Open Enum Support Both the power and the pain of enums come from their rigidity. Defining a fixed scope helps guide users towards intended use cases and makes enhanced type-safety possible. But the time will come when you need to alter the accepted values of an enum in your API, and it will be painful. How do you alter the set of accepted values without breaking existing code? To soften the pain, some languages have native support for the concept of "open" enums (other languages don't, but you can achieve the same results with custom classes). Open enums allow for unknown values to be passed through without erroring. For APIs that are rapidly evolving, this can help prevent some of the typical pain associated with making changes. And now for TypeScript, Python and Go, Speakeasy supports open enums. You can use our new extension: `x-speakeasy-unknown-values: allow` and your SDK will generate a type that includes the enum values and a catch-all type that represents any unrecognized value. This allows you to handle unknown values gracefully in your code. For the deep dive, read [our release post](/post/open-enums) --- ## 🚢 Improvements and Bug Fixes 🐛 Based on the most recent CLI version: [**Speakeasy v1.297.0**](https://github.com/speakeasy-api/speakeasy/releases/tag/v1.297.0) ### The Platform 🚢 Standardize publishing secret names created by `configure publishing` command\ 🐛 Circular reference handling within unions\ 🐛 Don't panic on bad validation lineNums\ 🐛 ClientCredentials hooks incorrect import\ 🐛 Mitigations for Github rate limiting\ 🐛 Nested namespaces in usage snippets ### Typescript 🐛 Jest compatibility fixes and leaner error models for Typescript\ 🚢 Safer weakly typed unions ### Go 🚢 Support Open Enums in Golang\ 🚢 Safer weakly typed unions\ 🚢 Support pointer request objects ### Terraform 🚢 Improve terraform diff suppression by instantiating arrays to `[]`\ 🐛 Fix Set datatype in Terraform ### Java 🚢 Sandbox (Dev Containers) support for Java\ 🚢 Support for Server Sent Events\ 🐛 Correct logic when retries and pagination are used together ### C# 🚢 .NET8 LTS support for C#\ 🚢 Simplify SpeakeasyHttpClient configuration\ 🐛 General Usage snippet reliability improvemenets\ 🐛 Flattened security field sanitizing in usage snippets C# ### Python 🚢 Support Open Enums in Python # Designing your API: Find the RESTful sweet spot Source: https://speakeasy.com/blog/api-design import { Callout } from "@/mdx/components";

If you're looking for a more comprehensive guide to API design, you can read our REST API Design Guide.

What we call RESTful APIs today often refer to JSON over HTTP, which is an offspring of the RESTful APIs Roy Fielding defined in his [dissertation](https://web.archive.org/web/20250911154212/https://ics.uci.edu/~fielding/pubs/dissertation/fielding_dissertation_2up.pdf) in 2000. Back then, JSON was still just an idea, and the web was still in its infancy. The constraints Fielding defined in his dissertation were a reaction to the state of the web at the time, but they remain relevant to the web today. Rather than co-opting the term RESTful, JSON-over-HTTP APIs could have benefited from a new term that acknowledges their differences from Fielding's REST. Alas, the term RESTful stuck, and here we are. This article will explore what it means to be RESTful in 2025, why it matters, and how to find the sweet spot between adhering to RESTful principles and following established practices. First, let's clarify the difference between RESTful APIs and REST-like APIs. Fielding's original REST model defines six architectural constraints, liberally summarized as follows: 1. **Client-server architecture**: This separation allows the client and server to evolve independently as long as the interface doesn't change. 2. **Statelessness**: Each request from the client contains all the information the server needs to fulfill that request, easing server workload and improving scalability. 3. **Cacheability**: Responses are explicitly labeled as cacheable or non-cacheable, which helps reduce client-server interactions and improves performance. 4. **Uniform interface**: This constraint is broken into four subconstraints. - **Resource identification**: Resources are identified in requests, typically using URIs. - **Resource manipulation through representations**: Resources are manipulated through representations, such as JSON or XML. - **Self-descriptive messages**: Messages include all the information needed to understand them. - **Hypermedia as the engine of application state (HATEOAS)**: Responses include links to related resources, allowing clients to navigate the API dynamically. 5. **Layered system**: A client should be unable to tell whether it is connected directly to the end server or to an intermediary along the way. 6. **Code on demand (optional)**: Servers can extend client functionality by transferring executable code, like Java applets or client-side scripts. Adherence to these constraints distinguishes a truly RESTful API from one that is REST-like. ## How most REST-like APIs adhere to REST constraints If you're building a modern API, you're likely adhering to some of the REST model's constraints, even if you're not following them all to the letter. The key is to understand the principles behind REST and apply them in a way that makes sense for your use case. ### ✅ Client-server architecture: Followed by most REST-like APIs In the context of APIs, this means that the client and server communicate over a network, with the server providing resources and the client consuming them. This separation is central to RESTful design and may be why the term "RESTful" was adopted for APIs that follow this pattern. ```mermaid sequenceDiagram participant Client participant Server Client->>Server: Request Server->>Client: Response ``` ### ✅ Statelessness: Followed by most REST-like APIs Statelessness means that each request from the client to the server must contain all the information needed to fulfill that request. This constraint simplifies server logic and improves scalability by allowing servers to handle requests independently. Most APIs follow this constraint by requiring clients to include all necessary information in each request. ```mermaid sequenceDiagram participant Client participant Server Client->>Server: GET /orders/123 Server->>Client: 200 OK { "order": { "id": 123, "status": "shipped" } } ``` ### ✅ Cacheability: Followed by most REST-like APIs Cacheability allows responses to be explicitly labeled as cacheable or non-cacheable, reducing the need for repeated requests to the server. By specifying cacheability, APIs can improve performance and reduce server load. Most APIs follow this constraint by including cache-control headers in their responses. ```mermaid sequenceDiagram participant Client participant Cache participant Server Client->>Cache: GET /orders/123 Cache->>Server: GET /orders/123 Server->>Cache: 200 OK { "order": { "id": 123, "status": "shipped" } } Cache->>Client: 200 OK { "order": { "id": 123, "status": "shipped" } } Client->>Cache: GET /orders/123 Cache->>Client: 200 OK { "order": { "id": 123, "status": "shipped" } } ``` The first request retrieves the order from the server and caches it. The subsequent request is served from the cache, reducing the load on the server. ### ⚠️ Uniform interface: Partially followed by most REST-like APIs The uniform interface constraint is seen by many as the heart of REST. It defines a standard way to interact with resources, making APIs more discoverable and easier to use. This constraint is often broken down into four sub-constraints: ✅ **Resource identification**: Resources are identified in requests, typically using URIs. Followed by most APIs. ✅ **Resource manipulation through representations**: Resources are manipulated through representations, such as JSON or XML. Followed by most APIs. ⚠️ **Self-descriptive messages**: Messages include all the information needed to understand them. Through the use of media types, APIs can achieve this sub-constraint. Partially followed by most APIs. ❌ **Hypermedia as the engine of application state (HATEOAS)**: Responses include links to related resources, allowing clients to navigate the API dynamically. Rarely followed by APIs. The last two sub-constraints of uniform interfaces are often the most challenging to implement and are frequently omitted in practice. HATEOAS, in particular, is a powerful concept that applies extremely well to web APIs that serve human users. For example, HTML returned by a web server contains links that users can click to navigate the web. Take this HTML response as an example: ```bash curl --header "Accept: text/html" https://api.example.com/orders/123 ``` ```html Order 123

Order 123

Status: Shipped

View Order View Customer ``` In this case, the links are clickable, allowing users to navigate the API by following them. This is the essence of HATEOAS. Contrast this with a JSON response: ```bash curl --header "Accept: application/json" https://api.example.com/orders/123 ``` ```json { "id": 123, "status": "shipped", "links": [ { "rel": "self", "href": "/orders/123" }, { "rel": "customer", "href": "/customers/456" } ] } ``` In this example, the response includes links to the order itself and the customer who placed the order. By following these links, a client can navigate the API without prior knowledge of its structure. In practice, an SDK or client library would need to be aware of these links to provide a similar experience. From a developer experience perspective, this can be challenging to implement and maintain. Instead of implementing HATEOAS, many APIs rely on documentation to inform developers how to interact with the API. While this approach is more common, it lacks the dynamic nature of HATEOAS. ### ✅ Layered system: Followed by most REST-like APIs The layered system constraint allows for intermediaries between the client and server, such as proxies or gateways. This separation enhances scalability and security by isolating components and simplifying communication. Most APIs follow this constraint by permitting intermediaries between the client and server. ```mermaid sequenceDiagram participant Client participant Proxy participant Server Client->>Proxy: Request Proxy->>Server: Request Server->>Proxy: Response Proxy->>Client: Response ``` Since API requests are stateless and contain all the information needed to fulfill them, intermediaries can forward requests without needing to maintain session state. This is especially useful for load balancing and security purposes. ### ⚠️ Resource-oriented architecture: Followed by some REST-like APIs Each of the constraints above contributes to a resource-oriented architecture, where resources are identified by URIs and manipulated through representations. This architecture makes APIs more predictable and easier to use by following standard resource interaction patterns. Most APIs follow this constraint by organizing their resources into collections and items, with standard methods for interacting with them. This pattern is often reflected in an API's URL structure, where resources are presented as nouns, and instead of using verbs in the URL, API actions are represented by HTTP methods. For example: - `GET /orders`: Retrieve a list of orders. - `POST /orders`: Create a new order. - `GET /orders/123`: Retrieve order 123. - `PUT /orders/123`: Update order 123. - `DELETE /orders/123`: Delete order 123. Many API design guidelines recommend using resource-oriented URLs to make APIs more intuitive and easier to use. We'll explore this in more detail later in the article. ### ❌ Code on demand: Rarely used in the context of APIs We'll skip this constraint for now, as it's optional and rarely implemented in practice outside hypermedia-driven APIs. ## Why adherence matters Is this a cargo cult, or do these constraints actually matter? They do, and here's why: ### The principle of least astonishment There is a big difference between delighting users and surprising them. The principle of least astonishment states that the behavior of a system should be predictable and consistent. When your API behaves in a predictable way, like sticking to the standard ways of using HTTP methods and formats, you're making it easy for developers to use. They shouldn't have to spend hours figuring out quirky behaviors or unexpected responses - that just leads to headaches and wasted time. ### Scalability Scalability is essential when designing APIs that need to handle varying loads and growth over time. Adhering to REST principles inherently supports scalability in several ways: - **Statelessness**: Without the need to maintain session state, servers can handle requests independently, making it easier to scale horizontally. - **Cacheability**: REST APIs explicitly label responses as cacheable. This reduces the server load, as cached responses can be reused from previous requests. - **Layered system**: REST architecture allows the deployment of intermediary servers, such as load balancers and cache servers, which isolate client requests from direct backend processing. ### Maintainability The constraints of the REST model naturally lead to a design that is easier to update and manage: - **Resource-oriented design**: By focusing on resources rather than actions, APIs become more modular and logically structured. - **Independent client and server evolution**: The client-server separation supported by REST allows both sides to evolve independently. ## Quantifying REST adherence [The Richardson Maturity Model](https://martinfowler.com/articles/richardsonMaturityModel.html) provides a framework for evaluating an API's compliance with RESTful principles. This model outlines four levels of RESTfulness, each building on the previous one, allowing you to assess and improve your API's design objectively: ### Level 0: The swamp of POX (Plain Old XML) At this base level, APIs typically rely on a single URI and use HTTP merely as a transport protocol. There is little to no differentiation in the use of HTTP methods, and operations tend to be defined solely by the payload. This resembles the remote procedure call over HTTP (RPC over HTTP) protocol, where the rich set of HTTP features, like methods and status codes, are underutilized. An example of a Level 0 API might be: ```bash curl -X POST https://api.example.com/api.aspx?method=createOrder -d "..." ``` ### Level 1: Resources The first step towards RESTfulness is exposing resources via distinct URIs. At Level 1, APIs start organizing data into resources, often with a collection-item structure, making the API more resource-oriented. Each resource, such as `/orders` or `/customers`, typically represents a collection of items, with individual resources accessible by identifiers like `/orders/{orderId}`. An example of a Level 1 API might be: ```bash # Retrieve a list of orders curl -X POST https://api.example.com/orders # Retrieve order 123 curl -X POST https://api.example.com/orders/123 ``` Note how the API is starting to use URIs to represent resources, but the use of POST for retrieval is not ideal. ### Level 2: HTTP verbs At this level, RESTful APIs progress by using HTTP methods (like GET, POST, PUT, and DELETE) to perform operations on resources. Level 2 APIs respect the semantics of these verbs, leveraging the full power of HTTP to perform operations that are predictable and standardized. For example, GET retrieves data, POST creates new resources, PUT updates existing resources, and DELETE removes them. An example of a Level 2 API might be: ```bash # Retrieve a list of orders curl -X GET https://api.example.com/orders # Retrieve order 123 curl -X GET https://api.example.com/orders/123 # Create a new order curl -X POST https://api.example.com/orders -d "..." # Update order 123 curl -X PUT https://api.example.com/orders/123 -d "..." # Delete order 123 curl -X DELETE https://api.example.com/orders/123 ``` ### Level 3: Hypermedia controls (HATEOAS) HATEOAS distinguishes Level 3 APIs from the rest. At Level 3, responses include hypermedia links that offer clients dynamic navigation paths within the API. This allows for discoverability directly embedded in API responses, fostering a dynamic interaction model in which clients can follow links to escalate through states or access related resources. ## Evaluating your API's RESTfulness To evaluate the RESTfulness of your API, consider the following questions: 1. Level 1: Are resources uniquely identified by URIs? For example, `/orders/123` uniquely identifies an order resource. 2. Level 2: Are all URLs resource-oriented, and do they use standard HTTP methods? For example, URLs should use GET to retrieve resources, POST to create new resources, PUT to update existing resources, and DELETE to remove resources. None of your endpoint URLs should contain verbs or actions. For example, neither `/cancelOrder` nor `/orders/123/cancel` is resource-oriented,. 3. Level 3: Do responses include hypermedia links for dynamic navigation? For example, a response might include links to related resources, allowing clients to navigate the API without prior knowledge of its structure. ## The RESTful sweet spot We believe the RESTful sweet spot lies somewhere between Level 1 and Level 2 of the Richardson Maturity Model. This is where most APIs can find a balance between adhering to RESTful principles and practicality. In case it wasn't crystal clear from the previous sections, we think HATEOAS isn't practical or relevant for most APIs. It was a powerful concept when the web was young, and APIs were meant to be consumed by humans. Here's what we recommend: 1. **Embrace resource-oriented design**: Think in terms of resources rather than actions. Identify the key resources in your API domain and structure your endpoints around them. This ensures your API is predictable and intuitive, making it easier for developers to understand and use. ✅ Good: Use resource-oriented URLs like `/orders` and `/customers`. ```yaml filename="openapi.yaml" paths: /orders: # Resource-oriented get: summary: Retrieve a list of orders post: summary: Create a new order /orders/{orderId}: # Resource-oriented get: summary: Retrieve order details patch: summary: Update an order put: summary: Replace an order delete: summary: Delete an order ``` ❌ Bad: Don't use action-based URLs like `/cancelOrder`. ```yaml filename="openapi.yaml" paths: /cancelOrder: # Not resource-oriented post: summary: Cancel an order ``` ✅ Compromise: Use sub-resources like `/orders/{orderId}/cancellations`. If you must include actions in your URLs, consider using sub-resources to represent them as resources in their own right. ```yaml filename="openapi.yaml" paths: /orders/{orderId}/cancellations: # Resource-oriented post: summary: Create a cancellation for an order ``` ✅ Less ideal compromise: Use top-level actions like `/orders/{orderId}/cancel`. ```yaml filename="openapi.yaml" paths: /orders/{orderId}/cancel: # Resource-oriented with action post: summary: Cancel an order ``` 2. **Use standard HTTP methods wisely**: Choose the appropriate method for each operation - GET for retrieval, POST for creation, PATCH for partial updates, PUT for complete updates or replacement, and DELETE for removal. Ensure your API follows the semantics of these methods. This allows developers to predict how operations will behave based on the HTTP method. It also implies idempotency, safety, and cacheability where applicable. An operation is considered **safe** if it doesn't modify resources. It is **idempotent** if the result of performing it multiple times is the same as performing it once. It is **cacheable** if the response can be stored and reused. ```yaml filename="openapi.yaml" paths: /orders: get: # Safe, idempotent, cacheable summary: Retrieve a list of orders post: # Unsafe, potentially idempotent, not cacheable summary: Create a new order /orders/{orderId}: get: # safe, idempotent, cacheable summary: Retrieve order details patch: # unsafe, potentially idempotent, not cacheable summary: Update an order put: # unsafe, idempotent, not cacheable summary: Replace an order delete: # unsafe, idempotent, not cacheable summary: Delete an order ``` 3. **Document thoroughly with OpenAPI**: Instead of relying on HATEOAS for dynamic navigation, use OpenAPI to provide comprehensive documentation for your API. OpenAPI allows you to define your API's structure, endpoints, methods, parameters, and responses in a machine-readable format. This ensures clarity and type safety for developers using your API. ### How targeting SDK generation enables better API design While you're designing your API, consider how it will be consumed by developers. If you're providing an SDK or client library, you can optimize your API design to make SDK generation easier and more effective, and you may find the optimized design also leads to a more RESTful API. We often see APIs with action-based URLs like `/orders/{orderId}/cancel` or `/orders/{orderId}/refund`. While partially resource-oriented, these URLs include actions as part of the URL. Firstly, these URLs are not as maintainable as resource-oriented URLs. If you decide to allow multiple cancellations for an order - for example, when an order is restored after being canceled and then canceled again - you may wish to represent cancellations as resources in their own right. This would lead to URLs like `/orders/{orderId}/cancellations/{cancellationId}`. Alternatively, you may wish to allow partial refunds, leading to URLs like `/orders/{orderId}/refunds/{refundId}`. Secondly, these URLs are not as predictable as resource-oriented URLs. Developers may not know which actions are available for a given resource, leading to a reliance on documentation or trial and error. From a design perspective, this could look like the following: ```yaml filename="openapi.yaml" paths: /orders/{orderId}/cancellations: post: summary: Set the order status to cancelled /orders/{orderId}/refunds: post: summary: Create a refund for the order requestBody: content: application/json: schema: type: object properties: amount: type: number minimum: 0 maximum: 100 ``` Then, in a future version of your API, you could introduce cancellations and refunds as resources in their own right: ```yaml filename="openapi.yaml" paths: /orders/{orderId}/cancellations: get: summary: Retrieve a list of cancellations for the order post: summary: Create a new cancellation for the order /orders/{orderId}/cancellations/{cancellationId}: get: summary: Retrieve a specific cancellation for the order /orders/{orderId}/refunds: get: summary: Retrieve a list of refunds for the order post: summary: Create a new refund for the order /orders/{orderId}/refunds/{refundId}: get: summary: Retrieve a specific refund for the order ``` This approach allows you to evolve your API over time without breaking existing clients. ## Going beyond RESTful principles While adhering to RESTful principles is essential, it's also important to consider the practicalities of API design. Here are some detailed topics we'll explore in future articles: 1. [**Pagination**](/api-design/pagination): How to handle large collections of resources. Should you use offset-based pagination, cursor-based pagination, or something else? 2. [**Filtering and searching**](/api-design/filtering-responses): How to allow clients to filter and search resources efficiently. 3. [**Error handling**](/api-design/errors): How to communicate errors effectively and consistently. 4. **Versioning**: How to version your API while maintaining backward compatibility. 5. **Security**: How to secure your API using authentication and authorization mechanisms. 6. **Rate limiting**: How to protect your API from abuse by limiting the number of requests clients can make. 7. **Webhooks**: How to implement webhooks for real-time notifications. Be sure to check back for more insights on API design. # api-experts-akshat-agrawal Source: https://speakeasy.com/blog/api-experts-akshat-agrawal ### TL;DR - Spend time thinking about your API taxonomy. Talk to users to make sure the language you use matches with how they understand your product. - Sometimes you need to build endpoints that deviate from the long-term vision of your API to deliver value to clients. That's okay, just always have a plan to bring things back into alignment down the road. - Even if you're building an API product, some tasks are more easily solved with a UI, be strategic about what you ask user's to do via API and know where a UI is important. - For a good DevEx, relentlessly focus on minimizing your time to ‘wow'. - If you're PM'ing an API product, you need to build with your API to uncover the points of friction. ## Introduction _Akshat Agrawal, is an MBA candidate at Harvard Business School. Prior to pursuing an MBA, Akshat worked as a senior product manager at_ [_Skyflow_](https://www.skyflow.com/)_. Skyflow is a data vault accessible via an API interface. Akshat was an early product hire at the company and was tasked with building and refining the MVP of Skyflow's API interface._ _Prior to joining Skyflow, Akshat worked as a PM for Google Stadia, Google's next generation cloud gaming platform._ **_Can you let people know what Skyflow does?_** Of Course. So, Skyflow is a Data Vault delivered as an API. There's a lot packed in that sentence, so let me break it down a little bit. First of all, what is a Data Vault? This is an emerging category of data infrastructure. Instead of sticking your sensitive and non-sensitive data into the same database, which is traditionally how companies have been operating, the emerging paradigm is to create a separate construct called a Data Vault, specifically designed for you to store and compute sensitive data. Sensitive data includes things like PII and KYC (know your customer data), basically anything you wouldn't want breached. Skyflow provides data vaults that have all the bells and whistles you'd want: encryption, key rotation, data governance, data redaction, field masking, all of that is baked in. It is a totally managed service that we deploy within our cloud for most of our customers. Then the way that developers interact with the data stored in the vault is through an API. So at Skyflow the API is a big part of the product, in some sense it is the product. The API is used for the entire lifecycle of interaction with your data vault, from creating the data vault, to specifying the schema to actually reading, writing, deleting data from the vault, as well as computing on that data, sharing that data, configuring governance rules. It's all meant to be done via API. **_What does API development look like at Skyflow? Who is responsible for releasing APIs publicly?_** Yeah, that's a good question. Well as you might expect, as a startup the API development process isn't perfect. When I was at Skyflow, we were definitely still figuring it out. But in general, there are a couple of key steps. First is designing the taxonomy (structure) of the API. This is a bit of a nuance to Skyflow's business, but because we are a data platform, we actually don't have a fixed schema. It's up to the customer to define what their schema looks like. You know, how they want to arrange their tables and columns. And that makes it very different from your typical REST-based API. We had to design an API that was generic enough to be able to serve whatever data schema our customers designed. And so this really exacerbates the taxonomy problem. We had to make sure that we were really thoughtful with the terms we used to describe our resources. We had to make sure that the structure of the API made intuitive sense and that whenever we add a new endpoint it's a natural extension of the existing taxonomy. So step 1 was getting the right structure for the endpoints and parameters. Then step 2 would be the actual development, including testing and telemetry for the API. Then step 3 would be rollout. Depending on the nature of the API, we might offer it as a beta; and test with certain customers. We'd assess whether there were any back-compat issues that needed to be accounted for. Then last step, we would prepare the documentation. That is super important. We were rigurious with making sure that the documentation is up to date. There's nothing worse than stale documentation. And that, roughly, is the full scope of the API development process. **_How does Skyflow ensure that publicly released APIs are consistent?_** It starts with establishing a literal rulebook for API development, and getting the whole team to buy into using it. We created guidelines which contained rules about how APIs are named, how parameters are named, standard semantics, expected functionality, all that kind of stuff. Ironing out those rules enables developers building new endpoints to get it mostly right on the first go. That's really important for development velocity. Building the rulebook isn't a one-time activity, our API best practices document was ever-evolving. As time has gone on, we've gotten to something pretty prescriptive. So now, if you want to add a new endpoint, you should be able to get it pretty close to perfect just by adhering to the things in the document. And that rulebook is actively maintained by our API excellence committee. **_If I were to ask a developer building public APIs at Skyflow what their biggest challenge was, what do you think they would say?_** Well like I said. I think how you describe your API resources is important. And one specific challenge for Skyflow was maintaining intelligibility across clients and user personas. As an example, some users might call it consistency, while other user's called it quorum, and still other people might call it something else. It can be really challenging for them to understand the product when it's presented in unfamiliar terms. That challenge is not technical. It's more organizational and logistical, and it's especially hard when you're in startup mode. You've got tight deadlines and customers asking for endpoints that need to be shipped quickly. So really balancing the cleanliness and excellence of the API against time constraints and organizational constraints is really hard. And it's all compounded by the fact that your APIs can't really change easily once you launch them. **_What about the challenges facing the developers who consume Skyflow's APIs?_** We are a security and privacy product and there's some problems that can create for the developer experience. As an example, a lot of APIs can afford giving users a static key, but for Skyflow that's not the case. We have to take extra steps to make sure interactions with the API are safe. We use a jwt token, which you have to dynamically generate and that jwt token contains all the scopes and permissions that define what data you can interact with.The whole process of generating that token, and getting authenticated to the API is non trivial. You have to get a credentials file with a private key, then run a script on it to generate a token, then you include that token in your API requests. Asking users to perform all those steps creates friction. We saw we had a lot of drop off along the usage path, especially for developers who were just trying the API for the first time. To address that issue, we built a trial environment into our dev portal. We autogenerate a personal access token that users can use to experiment with the API. But in production, we want to stick with that really robust, secure mechanism. ## API architecture Decisions **_Skyflow's public APIs are RESTful, Can you talk about why you decided to offer Restful APIs over say, GraphQL?_** I think for our use case, specifically, GraphQL is actually a good option, we are trying to appeal to developers at other tech companies. But ultimately it's still a bit ahead of the market. I think GraphQL is really cool, but you know, compared to REST, it's not as easy to understand or as commonly known by developers. So REST still felt like a natural starting point. GraphQL could always be added in the future. **_If someone was designing their API architecture today, what advice would you give them?_** Yeah, totally. I think API security is something everyone should spend more time on. APIs are the attack vector for a lot of the data breaches which are happening today. It's really hard to secure the entire surface of your API because unlike traditional services, which are running in a confined environment, APIs try to be universally accessible. I wish I had some more concrete advice, but if you're a startup, spend time discussing it as a team. ## PM'ing APIs **_What's it like being a product manager working on an API product?_** I do think, relative to other PM roles, you need to be more technical. That being said, it shouldn't be a barrier for most people. If you're at the point where you can make an API request, then you can probably develop and grow into the role. So I definitely would tell people to not let the fear of not being an engineer discourage you. Like any good PM, you should be a consumer of your own product. And for an API product, that means you need to use your API. That's the real way that you figure out where the friction points are. And when I say use, I mean really build with your product. Of course, you can get out Postman, get an API token and make an API call. But when you actually go to build something, you start to discover all these little thorns. I built with our API from day one.I had little side projects. In the case of Skyflow, that helped me identify that the definition of the schema was a pain. It was quite difficult to do via API. So we actually built a web UI that could do the schema definition. So, yeah, the TLDR is you have to build stuff with your API. And of course not specific to an API per say, but at the end of the day, making your early customers and partners successful is the most important thing, so let that guide you. Oftentimes, we would have to make hard trade-offs, keeping the shape of the API completely standard vs. giving a client specific functionality they needed. If you're PM'ing an API, your challenge will lie in eventually bringing everything back into a state of alignment. You're managing an ever expanding body of endpoints and functionality and so you really need to be thinking about the long-term. How will we hurtle the cattle and bring the endpoints back into a clean shape as a cohesive product? **_What were the KPIs that you tracked against for your product area?_** I focused a lot on developer experience. The most important metric that I tracked was our user's average time to ‘wow'. For a lot of API products, ‘wow' will be the first successful API request. The time from when you click ‘get started' to when you make that first successful API request, we put a lot of effort into trimming that time down. And it can't just be the sum of every step of the process: getting your credentials to the account, setting up your data vault, authenticating a token, creating a well formed API request, sending it debugging. You also need to account for the time where your user is stuck or inactive. They don't see anything in the documentation, so they give up and come back to it in a day. Because the documentation is part of your product. Reaching out to support and waiting for support to get back to me, that counts against time to wow. Through a combination of working on authentication, onboarding, documentation, API design, we were actually able to get that down to minutes. For developers, short time to ‘wow' inspires confidence. And it has a sort of dopamine effect. We feel good when we get that 200 after sending the API request. I don't know about you, but anytime I start to use a new API. I have this sense of foreboding. I'm just waiting to run into an error or stale docs or something like that. And when it just works the first time, it's an amazing feeling. ## _DevEx for APIs_ **_What do you think constitutes a good developer experience?_** There's a lot of things that go into creating a good developer experience, but I would say that at the end of the day it boils down to productivity. A good developer experience is when developers can be productive. This is not just for APIs, this is for all dev tools. It's all about productivity. A lot of studies show with the proliferation of SaaS tools, Cloud Tools, microservices, new architectures, developer productivity has kind of hit rock bottom. There's just so much to account for. The amount of hours that developers spend doing random stuff besides just writing and testing good code is way off the mark. The proliferation of APIs should have big benefits in the long term, but currently developers are losing a lot of time understanding partner APIs, looking for documentation, debugging, dealing with authentication. So when it comes to APIs, a good developer experience is one that makes developers maximally productive when developing atop your API. Unlike most products, with an API you want your users spending as little time as possible with your API. It's not their job to spend a lot of time on your API. They're just using your API to get something done. So you really want them kind of in and out the door, so to speak. ## Closing **_A closing question we like to ask everyone: any new technologies or tools that you're particularly excited by? Doesn't have to be API related._** That's a good question. And I think a big part of being at grad school is my desire to explore that fully. I've been really deeply in the world of security, privacy, data and fraud for the last few years. I'm excited to see what else is out there. One thing that's really interesting to me right now is climate tech. I think there's just so much scope for software to make an impact in mitigating climate change and so I'm really excited to explore that space more. # api-experts-clarence-chio Source: https://speakeasy.com/blog/api-experts-clarence-chio ### TL;DR - Tech writing teams are often papering over the gaps created by immature API development processes. - As businesses grow, the complexity of managing even simple API changes grows exponentially. - Designing an API is no different from building any product. The question of what framework you use should be dictated by the requirements of your users. - Don't build an API just to have one. Build it when you have customer use cases where an API is best suited to accomplishing the user's goal. - Good devex is when your services map closely to the intention of the majority of your users. - The mark of good devEx is when you don't have to rely on documentation to achieve most of what you're trying to do. ## Introduction [_Clarence Chio_](https://www.linkedin.com/in/cchio/)_, is the CTO and one of the co-founders of Unit21._ [_Unit21_](https://www.unit21.ai/) _is a no-code platform that helps companies fight transaction fraud. Since the company started in 2018 it has monitored over $250 billion worth of transactions, and been responsible for preventing over $1B worth of fraud and money laundering._ _In addition to his work on Unit21, Clarence is a Machine Learning lecturer at UC Berkeley and the author of_ [_“Machine Learning & Security”_](https://www.oreilly.com/library/view/machine-learning-and/9781491979891/) ## API Development @ Unit21 **_What does API development look like at Unit21? Who is responsible for releasing APIs publicly?_** API development is a distributed responsibility across the different product teams. Product teams build customer-facing features which take the form of dashboards in the web interface, API endpoints, or both. So generally, every product team maintains their own sets of API endpoints defined by the scope of the product vertical that they're working on. But the people that make the decisions defining best practices and the consistency of how our APIs are designed is typically the tech leads working on our data ingestion team. As for who maintains the consistency of how APIs are documented and presented to our customers, that is our technical writing team. **_How come the data ingestion team owns API architecture is that just a quirk of Unit21 engineering? Is there any resulting friction for API development in other parts of the engineering organization?_** Yeah, the reason for this is pretty circumstantial. Most of the API usage that customers engage with are the APIs owned by our data ingestion team. We're a startup and we don't want too much process overhead, therefore we just let the team that owns the APIs with the most engagement, maintain the consistency and define what the API experience should be. As for friction with other parts of the org, it's largely been okay. I think the places of friction aren't specifically related to having designated a specific product team to be the owner. A lot of the friction we've had was down to lack of communication or immature processes. A common example is when the conventions for specific access patterns, or query parameter definitions aren't known across the org. And if the review process doesn't catch that, then friction for the user happens. And then we have to go back and clean things up. And we have to handle changing customer behavior, which is never fun. However, it's fairly rare and after a few incidents of this happening, we've started to nail down the process.. **_What is Unit21's process to ensure that publicly released APIs are consistent?_** There is a strenuous review process for any customer facing change, whether it's on the APIs or the dashboards in the web app. Our customers rely on us to help them detect and manage fraud so our product has to be consistent and reliable. The API review process has a couple of different layers. First we go through a technical or architectural review depending on how big the changes; this happens before any work is done to change the API definitions.. The second review is a standard PR review process after the feature or change has been developed. Then there's a release and client communication process that's handled by the assigned product manager and our technical writing team. They work with the customer success team to make sure that every customer is ready if there's a breaking change. **_If I were to ask a developer building public APIs at Unit21 what their biggest challenge was, what do you think they would say?_** It's probably the breadth of coverage that is growing over time. Because of the variance in the types of things that our customers are doing through our API and all the other systems and ecosystems that our customers operate in, every time we add a new API the interactions between that API and all the other systems can increase exponentially. So now the number of things that will be affected if, for example, we added a new field in the data ingestion API is dizzying. A new field would affect anything that involves pulling this object type. Customers expect to be able to pull it from a web hook from an export, from a management interface, from any kind of QA interface, they want to filter by it, and be able to search by it. All those things should be doable. And so the changing of APIs that customers interact with frequently cause exponentially increasing complexity for our developers. **_What are some of the goals that you have for Unit21's API teams?_** There's a couple of things that we really want to get better at. And by this, I don't mean a gradual improvement on our current trajectory, but a transformational change. Because the way we have been doing certain things hasn't been working as well as I'd like. The first is the consistency of documentation. When we first started building our API's out, we looked at all the different types of spec frameworks, whether it was Swagger or OpenAPI, and we realized that the level of complexity that would be required if we wanted to automate API doc generation support would be too much ongoing effort to be worthwhile. It was a very clear answer. But as we continue to increase the scope of what we need to document and keep consistent, we realized that now the answer is not so clear. Right now this issue is being covered over by our technical writing team working very, very closely with developers. And the only reason this work is because our tech writer is super overqualified. She's really a rock star that could be a developer if she wanted to. And we need her to be that good because we don't have a central standardized API definition interface; everything is still defined as flask routes in a Python application. She troubleshoots and checks for consistency at the time of documentation because that's where you can never hide any kind of inconsistency. But this isn't the long term solution, we want to free up our tech writers for differentiated work and fix this problem at the root. We're still looking into how to do this. We're not completely sold on Swagger or OpenAPI, but if there are other types of interfaces for us to standardize our API definition process through, then potentially, we could find a good balance between achieving consistency in how our APIs are defined & documented and the efforts required from our engineering team. But yeah, the biggest goal is for more documentation consistency. ## API architecture Decisions **_When did you begin offering APIs to your clients, and how did you know it was the right time?_** When we first started, we did not offer public APIs. We first started, when we realized that a lot of our customers were evolving use cases that needed a different mode of response from our systems. Originally, we would only be able to tell them that this transaction is potentially fraudulent at the time of their CSV, or JSON file being uploaded into our web UI, and by then the transaction would have been processed already. In many use cases this was okay, but then increasingly, many of our customers wanted to use us for real time use cases. We developed our first set of APIs so that we could give the customer near real time feedback on their transactions, so that they could then act on the bad event and block the user, or block the transaction, etc. That was the biggest use case that pushed us towards the public API road. Of course, there's also a bunch of other problems with processing batch files. Files are brittle, files can change, validation is always a friction point. And the cost of IO for large files is a big performance consideration. Now we're at the point where more than 90% of our customers use our public APIs. But a lot of our customers are a hybrid; meaning they are not exclusively using APIs. Customers that use APIs also upload files to supplement with more information. And we also have customers that are using our APIs to upload batch files; that's a pretty common thing for people to do if they don't need real time feedback. **_Have you achieved, and do you maintain parity between your web app and your API suite?_** We don't. We actually maintain a separate set of APIs for our private interfaces and our public interface. Many of them call the same underlying back end logic. But for cleanliness, for security, and for just a logical separation, we maintain them separately, so there is no parody between them in a technical sense. **_That's a non-standard architecture-choice, would you recommend this approach to others?_** I think it really depends on the kind of application that you're building. If I were building something where people were equally likely to interact through the web as through the API, then I think I would definitely recommend not choosing this divergence. Ultimately, what I would recommend heavily depends on security requirements. At Unit21 we very deliberately separate the interfaces, the routes, the paths between what is data ingestion versus what is data exfiltration. That gives us a much better logical separation of what kinds of API routes we want to put into a private versus public subnet, and what exists and just fundamentally does not exist as a route in a more theoretically exposed environment. So ultimately, it's quite circumstantial. For us, I would make the same decision today to keep things separate. Unless there existed a radically different type of approach to API security. I mentioned earlier there were a couple of things that we would like to do differently. One of them is something along this route. We are starting to adopt API gateways and using tools like Kong to to give us better control over API access infrastructure, and rate limiting. So it's something that we're exploring doing quite differently. **_Unit21's public APIs are RESTful, Can you talk about why you decided to offer Restful APIs over say, GraphQL?_** I think that designing an API is very similar to building any other product that's customer facing, right? You just need to talk to users. Ask yourself what would minimize the friction between using you versus a competitor for example. You should always aim to build something that is useful and usable to customers. For Unit21 specifically, the decision to choose REST was shaped by our early customers, many of whom were the initial design partners for the API. We picked REST because it allowed us to fulfill all of what the customers needed, and gave them a friendly interface to interact with. I've been in companies before where GraphQL was the design scheme for the public facing APIs. And in those cases the main consumer persona of the APIs were already somewhat acclimatized to querying through GraphQL. Asking people that haven't used GraphQL to start consuming a GraphQL API is a pretty big shift. It's a steep learning curve, so you need to be really thoughtful if you're considering that. **_Also, you've recently released webhooks in beta, what was the impetus for offering webhooks, and has the customer response been positive?_** That's right. Actually, we've had webhooks around for a while, but they hadn't always been consistent with our API endpoints. We recently revamped the whole thing to make everything consistent, and that's why it's termed as being ‘beta'. Consistency was important to achieve because, at the end of the day, it's the same customer teams that are dealing with webhooks as with the push/pull APIs. We wanted to make sure the developer experience was consistent. And since we made the shift to consistency, the reaction has been great, customers were very happy with the change. ## Developer Experience **_What do you think constitutes a good developer experience?_** Oof this is a broad question, but a good one. All of us have struggled with API Docs before; struggled with trying to relate the concept of what you're trying to do with what the API allows you to do. Of course, there are some really good examples of how documentation can be a great assist between what is intuitive and unintuitive. But I think that a really good developer experience is when the set of APIs maps closely to the intention of the majority of the users using the API, so that you don't have to rely on documentation to achieve most of what you're trying to do. There are some APIs like this that I've worked with before that have tremendously intuitive interfaces. In those cases, you really only need to look at documentation for exceptions or to check that what you're doing is seen. And I think those APIs are clearly the result of developers with a good understanding of not only the problem you're trying to solve, but also the type of developers who will be using the API. ## Closing **_A closing question we like to ask everyone: any new technologies or tools that you're particularly excited by? Doesn't have to be API related._** Yeah, I think there's a bunch of really interesting developments within the datastream space. This is very near and dear to what we're doing at Unit21. A lot of the value of our company is undergirded by the quantity and quality of the data we can ingest from customers. We're currently going through a data architecture, implementing the next generation of what data storage and access looks like in our systems, and there's a set of interesting concepts around what is a datastream versus what is a database. And I think we first started seeing this become a common concept with K sequel, in confluent, Kafka tables etc. But now with concepts like rocks set with, snowflake and databricks all releasing products that allow you to think of data flowing into your system as both a stream and a data set. I think this duality allows for much more flexibility. It's a very powerful concept, because you no longer have to think of data as flowing into one place as either, but it could be both supporting multiple different types of use cases without sacrificing too much of performance or storage. # api-experts-jack-reed Source: https://speakeasy.com/blog/api-experts-jack-reed ## TL;DR - It's great to have an industry standard like OpenAPI for API development, but the flexibility in the spec has made true consensus elusive. - If you're building an SDK, the goal is to make it as easy as possible for users to make their first request. - When you're building in a regulated space, it's important to offer a sandbox to help customers build quickly and safely. - At Stripe, maintaining the [API platform](/post/why-an-api-platform-is-important/) required a large team. In the last couple years this API DevEx space has started to take off and democratize API product quality. It's a welcome development for people who care about this type of stuff. ## Introduction _Jack Flintermann Reed is an engineer at [Increase](https://increase.com/), a company that provides an API which enables developers to build a bank. Jack spends his time thinking about how to make the [Increase API](https://increase.com/documentation/api#api-reference) enjoyable for customers to use, and scalable for the Increase team to develop. Before he joined Increase, Jack was a staff engineer at Stripe._ ## APIs at Increase **Increase offers a REST API to developers, how did you decide that was the right interface?** At one point we tried offering both a REST API and a GraphQL API but we found operating the GraphQL API to be challenging and ended up winding it down. There was a cognitive overhead for the user that was hard to overcome: like, welcome to our GraphQL API, here are all the libraries you're gonna have to install before you get started. And you know, GraphQL libraries across different programming languages have really varying quality. The Ruby GraphQL client is okay for example, but other languages not so much. And then, client aside, you need to learn all these new concepts before you can even do what you want. For users it's like, “I just want to make an ACH transfer here.” And that's the really nice thing about REST. Every single language has an HTTP client built into it. There is no stack required to get started. It is really simple. You can play with a lot of REST APIs in the browser's URL bar if you want to. And that makes getting started with documentation and integration a lot simpler. So the barrier to entry is lowest on REST. And, that's not to say it doesn't come with plenty of issues of its own, which I'm sure we will get into. But we think it's what is right for us right now. Ultimately, one of the reasons we had to give up on GraphQL is we realized that because of the level of DevEx we wanted to give users, we only had the bandwidth to do one public API really well, and so we were forced to pick. I suppose that is one of the main reasons Speakeasy exists, right? **Yes it is. And to explicitly state something that I think you're implying, the developer tools that are needed to support a REST API and a Graph QL API, are fairly distinct?** I think so. And again it's partially down to the maturity of the technology. For example, when we think about the SDKs that we want to offer to our users, one of the things that we feel pretty strongly about is not having any dependencies. And that's just not possible with a GraphQL API. Like, you're not going to write your own GraphQL client, you're going to bundle something in. But that exposes you to problems where there could be a version conflict with the client you've pinned yourself to. And with REST, it's at least possible to avoid that problem. We've recently written a Ruby SDK for our API, and we're pretty happy that it's got no external dependencies. **Are you versioning your API, do you plan to?** We don't version the API yet. We are small enough that we just haven't had to do it. We inevitably will have to, but I'm trying to delay it for as long as possible. It's a lot of work, and there's just not a canonical way to do versioning. So I'm cheating by just making it a problem for my 2023 self. We do occasionally deprecate things, but we're close enough to our users right now that we can just reach out to them. And for us right now, we're in a phase where the API is mostly additive, we're still building out the core resources. The deprecations we've done have been at the API method level. It's easy enough for us to handle. The real nightmare is when you want to take out this field from this API response. And you have no idea if anyone is relying on it or not. That's the one that sucks. And fortunately, we haven't had to do that. And we are trying really hard to not have to do that. A good way to save yourself trouble is by being really conservative about what you put in your API to begin with. That's our approach right now, we want users to pull features out of the API. Yes we have the data, we obviously could expose that field, but until a few people are asking us for it, we are not going to put it into the API. ## Thoughts On OpenAPI **Do you like OpenAPI? Does your team maintain an OpenAPI spec?** I'm glad it exists, we publish an open API spec. And it is going to be the foundation of all of the stuff that you and I are talking about right now. We're going to generate our documentation and clients from our OpenAPI spec. That said, I think it's extremely difficult to work with. So, I'm glad that there is a standard, but I wish the standard were better. I think OpenAPI has taken this nice big-tent approach where anyone can describe their API with OpenAPI. But there are some crazy APIs out there, right? And so there are a million features inside OpenAPI. I've been working in the API space for a while and it took me some pretty serious effort to try and understand OpenAPI and how to get started. There are a lot of features you could use, but there's no easy way to separate out the set of features that you should use. One example that I always harp on is null. If I have a nullable parameter, there's at least four ways to represent that in OpenAPI 3.1. But not every library implements the full OpenAPI specification, so the tools and libraries that I want to have consume my spec might only support a certain way to represent null. So while it's all well and good that you can represent null 4 different ways, if you actually use two of them, none of your tooling will work. And that type of opinionated information is extremely hard to pin down. **Do you generate your OpenAPI spec from code?** Yeah, we have this cool library we use internally, I'm pretty proud of it. If I had infinite time, I think there's probably a great little framework that could be pulled out of this. Increase is written in Ruby, and we use [Sorbet](https://sorbet.org/) from Stripe (a type checker for Ruby). We've built a Domain-Specific Language, you might've seen other companies do something similar, where you can write API methods using a declarative syntax. You say, this API takes a parameter called, “name”, and it's a string, and this one takes “age”, it's an integer. And this library then goes and produces a couple of outputs. On the one hand, it gives you an OpenAPI spec. Then on the other, it handles all the shared application concerns around parsing requests: error messages, type suggestions, etc. Everything you don't want to force individual developers at Increase to be thinking about. It will spit out into the application, a parsed, strongly-typed, parameters object. And so, the developer experience is pretty nice for folks at Increase. If you add a parameter, it just shows up in the docs and the SDKs without you having to do anything, which is the dream. It's an all in one generator for artifacts. ## How Does Increase Think About DevEx **You mentioned earlier that one of the guiding principles in your SDK generator development was limiting dependencies. What other principles do you have for SDK development?** We want to make it very easy to write your first request. And to achieve that, the majority of the library should be type definitions. It should list out the API methods and each of those should have a nice type definition that makes it easy to autocomplete when you're working. Then there's the other piece of the SDK, sort of the core, which actually handles making the requests. That should be very tunable, and ultimately, swappable. I worked at Stripe before coming to Increase. And at Stripe, when we were integrating with an external service, we'd rarely use the official SDK, because of all the things, like internal service proxies, that we would need to configure in order to safely get requests out of the stripe infrastructure. It would have been really nice to say, we have our own way of making HTTP requests, and I'm happy to write adapter code to conform to your interface; I'll just kind of ram it into your type definition. That would have been the sweet spot for us, and that experience has influenced our approach at Increase. We have written it to be the kind of SDK we would have wanted to integrate with. If people are interested in this stuff, there's [a really fantastic blog](https://brandur.org/articles) by a developer I used to work with at Stripe, Brandur. He's a prolific writer and often dives deep on the minutiae of good API design. If people haven't read it, they should. **What about DevEx for APIs? What's most important?** There's a lot of different things that go into it. The first piece is documentation, trying to make your system legible to others. Good documentation focuses on the concepts you need to know, without overwhelming you with information. I think at Increase, we do an okay job of this right now, but it's one of the things I most want to improve about our own product. We've got the API reference, which is great - it's the index at the back of the encyclopedia, but it's not enough. It's really important to know the typical path for building an integration with your API and then communicate: “here are the five things you're going to want to go and touch first.” I think that initial communication is more important than almost anything else to me when it comes to the experience of using an API. And again, I'm a major type system enthusiast. And that's because when you get it right with your developer tooling, your documentation can just flow into the developer's text editor. And to me, that's the dream experience. But it takes a lot of work to get there. That's all concerning understanding the API, and doing the initial integration. There's also the ongoing operation of the integration code. And that's about answering questions like: How do I see what I've done? Did I make a mistake? How do I debug my mistake? That's where tools that enable you to see the requests you've made are really useful. It's hard to say what specifically constitutes a great developer experience, but a lot goes into it, and, fortunately for developers, the bar for what great means gets higher every year. Tools are getting better. **Are there other companies that you look to as a north star when building DevEx?** So, obviously Stripe gets a lot of praise for being the first company here. They were so much better than anything else at the time they started. And I think if you look back at the things that they did, a lot of them were really small touches. Things like copy/paste for code examples, right? Small, but really nice. Another one was, if you misspell an API parameter, giving you an error message that suggests the correct spelling. It's not rocket science. But those little things add up into something really nice. I'm obviously really biased because I worked there, but I still think Stripe is the best. I learned a lot from my time there, so it's still the company I model after most. Besides Stripe, I wouldn't say there's one company that is necessarily a North Star. I have a loose list of peer companies. When I'm doing research, it's usually because I'm grappling with the fact there's a lot of under-specified things in building REST APIs and I want to see how others have handled it before I make a decision. I'm not interested in features or clever things that they've done that I want to copy, it's more about checking whether there is consensus on a topic. If there is a consensus, I want to follow the precedent so that it is not surprising to our users. However, I'm usually disappointed. There often isn't consensus on the way to do basic features in REST APIs. Which is funny and sad. Incidentally, that's one of the nice things about GraphQL. They tell you how to do everything. It's very proscriptive. **How is the value of Increase's investment in DevEx measured? Are there metrics you want to see improve?** It's not really obvious, it's much more of a qualitative metric. We open a Slack channel for every new user, and we stay pretty close to them during the integration. We're not quite looking over their shoulder, but if we could, we would. And so, we're pretty sensitive to how that kind of first experience goes. We try to get feedback from pretty much everybody who goes through it. And so, it's just things that we see mainly. Like if everyone is running into a similar stumbling block, we prioritize removing it. It's not like there's a KPIs dashboard on a wall. It's things we see in conversations with users every day. It scales pretty well if the whole team pitches in on working with users. Slack Connect is a pretty great tool. ## Increase's Journey **You guys are a startup, but you've been investing in your API's DevEx from the get go. Was that a conscious decision?** Yeah, again, It's pretty informed by our experiences at Stripe. It's also a consequence of the space we work in. We offer a set of banking APIs, and traditionally banking API integration is pretty laborious. With a traditional bank, it'll be an extended contracting phase, then you get to a signed contract, and finally you'll get this PDF for docs. And then the integration involves sending files back and forth between FTP servers. It's not fun. And so as a startup in the space, our goal is to make the integration timeline as short as possible, and as dev-friendly as possible. Even in the cases where we need a signed contract before a user can go live in production, we enable their development team to build while that's being sorted. **Ya, please expand on that a bit more. As a banking API there's inevitably compliance. How do you guys balance integration speed with compliance?** It depends on the use case - we can get most companies live in production very quickly. And then there are some that require more due diligence. That's where tooling like an API sandbox is helpful. With a sandbox, you can build your whole integration. The dashboard experience looks and feels real, and so you can just build out what you need while the legal stuff is handled. We've learned that it takes a lot of work to make a good sandbox. We have to do a lot of weird things to simulate the real world. For example, in the real world there's several windows throughout the day when the federal government processes ACH transfers and they go through. So if a developer makes an ACH transfer in the sandbox, what do we do? Should we simulate that happening immediately, or wait like the real world. There's not always a right answer. We actually built a simulation API, where you can simulate real world events in all kinds of tunable ways. And so that has been fascinating. It made us re-architect a fair amount of code to get it working. ## Closing **In the longer term, are there any big changes you think are coming to the way people interact with APIs?** Haha I think this question is above my paygrade. It's not quite the lens through which I think about things… But, I guess one thing that is interesting is how many companies are starting to pop up in this API DevEx space. It seems like there were zero people working on this stuff two, three years ago. Now there's a few. Before I joined Increase, I was thinking I might start a company of my own in the space. One of the ideas I was kicking around was a platform for webhook delivery. I've seen a bunch of new startups doing that over the interim 2 years. I think that's cool. The size of the team that maintained these things at Stripe was big. It required a lot of manual work. And so it's cool to see that level of API product quality being democratized a little bit. Like I said, I think the quality bar will continue to rise, and it has risen. But, today, it's still a pain. It still takes a lot of work, you really have to care about it. I'm hoping it becomes a little bit easier to buy off the shelf as time goes on. # api-experts-mathias-vagni Source: https://speakeasy.com/blog/api-experts-mathias-vagni ## TL; DR - Building your API to be public (without private endpoints) pays huge dividends in the form of extensibility for your customers. - GraphQL requires some extra legwork to cover the basics, but comes in handy for supporting more advanced UIs and apps. - The Plain team is not only working on their APIs' DevEx, but is trying to make it easy for customers to build their own API endpoints. - For smaller companies, the qualitative feedback on your developer experience is more useful than tracking metrics. ## Introduction to Plain **To warm up, could you give a brief overview of [Plain](https://plain.com/) and what y'all are building?** Plain is the customer support tool for developer tools. My co-founder Simon and I both found that existing customer service platforms were really hard to integrate with and fundamentally not built for engineering led products. Their APIs were afterthoughts tacked on and dumbed down to be made public. Their data models and workflows assumed that they were the primary store of customer data vs your own databases. These assumptions just didn't make sense, especially in the developer tools vertical. We wanted to build a customer service platform that is incredibly easy for developers to build with. The inspiration came from our experiences at Deliveroo (food delivery) and Echo, an online pharmacy. All of Plain is built API-first, meaning the API we make available to users is identical to the API we use internally. This means that there are no restrictions with what you can do programmatically. Anything you can do in Plain, you can do via the API. By dog-fooding our API this way, we end up constantly working on making it easier to build with and integrate Plain into your own systems. ## Architecture of the Plain API **How does being API-first impact product development? Move fast and break things doesn't work so well for APIs right?** Yeah, we have virtually no private surface area, which is a challenge since it means that everything we build is exposed. Things that are traditionally quite easy are harder, for example deprecating an endpoint, but the rewards are massive. You have to move a bit slower at the beginning, especially when you're conceiving a new feature. It forces you to think quite carefully as to where you put logic and where you ‘encode' your opinions if that makes any sense. For example, [our data model](https://docs.plain.com/data-model) is something that is integral and is set in the API. A less clear cut case is certain opinions around how you should work as someone providing support to users. We do our best to make sure that opinions are more in our UI and the Plain app, and the API remains more generic. As a result of this, we definitely have more in depth conversation around API design than you would normally see at an early stage startup. The payoff though is huge. When we onboard customers, and our platform is missing certain features, they are able to extend and just build the features themselves. As an example, we had one customer using Discord to manage their community. We didn't offer a Discord integration. So they built it. It's super cool, now they're able to talk to users in Discord and sync it to Plain. That's where you reap the benefits of an API first approach. **One of the first decisions an API company needs to make is REST or GraphQL, Plain is GraphQL-based. What persuaded you that GraphQL was best?** We realised early on that most companies want to use customer support APIs to automate only small parts of their workflow. For example when they have an issue in their back-end they might want to proactively reach out to that customer and so they might open a ticket. In these early stages of companies, the requirements for customer support tooling are quite simple so we were very tempted to use REST. For simple API calls REST is typically way easier. However at the same time we learnt that many people were looking for a productivity focused, power user friendly, support app. This is especially true for engineers as a demographic. Given this, GraphQL seemed like a much better fit. For building complex applications the schema stitching required by REST can be very painful and it was something we were keen to avoid. Ultimately weighing both of these conflicting requirements we went for GraphQL. We felt like if we start with a GraphQL API, we could always add a REST layer on top later. The reverse would be more difficult to do in a performant way. **I've spoken to some other founders who started with GraphQL, before switching to REST. One of their criticisms was that the ecosystem of GraphQL tooling left a lot to be desired. Yes, What has your experience been?** There are some things that are great. And then there are some things that you expect to be good, but are terrible. For basic apps, where you just want to make some queries and run some mutations, you're going to have a great time. You make some GraphQL files, generate all of your types. It can even generate clients for you. There's lots of libraries that take care of things like caching and normalisation, definitely in the React ecosystem, but also in others. So that's all fine. I think, where you start running into GraphQL weirdness and where the ecosystem leaves a lot to be desired is in terms of some more basic elements like error handling and complex input types. With REST APIs a 401 is an unauthorized request, you don't need to explain that to anyone. And because you are not restricted to a type system you can do things that are just plain awkward in GraphQL (e.g. union input types). **How do you handle errors in GraphQL then?** Unlike REST, GraphQL is multileveled so certain problems become harder. Suddenly, you might be in a situation where the user has top level permissions to get something, but then doesn't have permission for one of the nested elements. The way that you handle that error is DIY. There's no (good) convention to follow. We made some good and bad decisions in our early days. What helped was that very early on, we decided to write our own linting rules. GraphQL has an inbuilt tool that essentially parses the GraphQL schema when a request comes in, and with this, you can write rules to lint your schema. We write our schema first, so the linters enforce convention before any API code is actually written. And it's not just syntactical errors, our linters enforce things like, every mutation must return an error, an error must be of this type, etc. It's served us really well, because it means that we have very few debates on PRs around repeated API design topics. **What's been the client reaction to the GraphQL API?** It's interesting. I think the more complex the use case, the happier users are with GraphQL. There's obviously still a bit of a gap between GraphQL and REST when it comes to awareness, and we do encounter companies who want to use Plain where the engineers have never worked with GraphQL, beyond reading blog posts. It's definitely a barrier, but not insurmountable; it just requires a little bit more hand holding. We help customers through this by giving them code examples and instructions on how to make GraphQL requests, how our errors work, that kind of thing. Overall, we've found that as long as you've put work into developer experience and consistent API design, developers will pick things up quickly. And we are militant about the consistency and experience of our API. As an example, we provide incredibly thorough error messages, which is something that developers love. Once they start building they quickly realise: “Okay, this is solid.” ## Plain's API DevEx **That's a good segue. What additional tooling do you give to users to support API integration? What tooling improvements are you looking to make?** Before we dive in, it's worth knowing that there are two ways you build with Plain. There is the GraphQL API, which we've been talking about, but there's also something we call [Customer Cards](https://docs.plain.com/adding-context/customer-cards). They are really interesting, because they're the inverse of the traditional way that other support tools work. Instead of our customers calling us and syncing data (when the support tool is the primary source of truth), our users provide a URL, which we call to fetch _your_ customer data which is then loaded up within the Plain UI. ![customer-card image](/assets/blog/api-experts-mathias-vagni/customer-card.png) This means that when your support team is helping someone they instantly have access to context from your own systems about the customer they are trying to help. What account tier they are, what their recent activity has been, how much they're paying you every month, etc. We want that data to continue to live in our customers systems, so for the product to work, we need to help our customers construct an API endpoint which we can call. We've put in quite a bit of work into the DX of Customer Cards but I think our developer experience is a work in progress. It's a fairly novel workflow, so it's harder to do well than when trying to document an API. **How have you been trying to solve it so far?** I think we've made some good steps. We've built a playground that can assist users while they're building which is quite nice, but there's definitely more to do. This data transfer is async. It's happening all the time. And so error handling and the developer experience here is actually a lot more challenging than a traditional API. We have to provide a lot more visibility on errors and monitoring. We need to know if you responded with something wrong, and why it was wrong. We then need to notify you that it was wrong and persist it so that you can fix it. The same goes for timeouts and anything else that's unexpected. It's complicated and we've not totally solved this experience. **Do you offer SDKs for the Plain API?** We haven't yet, but we plan on it. We've been relying on every ecosystem having its own stack, for generating GraphQL tooling. But we plan on offering SDKs to make integration easier, and to make errors easier to understand and parse. We really want to get to a place where, for a simple use case, you don't have to deal with GraphQL at all. If you look at how Slack does it, it's very good. No one is actually making raw Slack API calls, right? They're all using the client and the playground provided to visually work out how to construct messages and do things with bots and so on. **Any other DevEx challenges that you're tackling?** I think on the API side, we've covered it, we really just want to make it easier and easier to integrate and use Plain. It's our raison d'être, I don't think we'll ever ‘finish' working on DevEx Outside of our API, we also have [a chat solution](https://docs.plain.com/support-channels/chat/overview), and we've spent a lot of time thinking about the DevEx there. If you're using a React app and you want to embed chat into your product, it's a UI-level integration, and that has its own challenges. If you look at how most support tools or chat providers allow you to embed, it's through a script tag. You add a tag, and a floating button appears on the bottom right. And that's your chat. In our world, we wanted to allow chat to be a bit more embeddable, to essentially look like it's part of your product and deeply look like its native. To do that, we've built a React package. It's been a tough nut to crack. Everyone has such specific expectations of how your embed should behave. And you're confronted with the multitude of different stacks and approaches people take. People do things that are super delightful, but unexpected. And that's where the complexity comes in when you are trying to deliver that seamless Developer Experience. **Are there metrics you track to measure impact of the work on your API's DevEx?** Not yet, we're still so focused on every customer that metrics don't really make sense yet. We track all the feedback we get meticulously. Even slight grievances with our API or our integrations we discuss thoroughly. That's a scale thing, largely. What's also interesting is, by focusing on developer tools as a customer base, the feedback we get is really, really good. You get feedback similar to when an in-house engineer reports a bug. So. much. detail. So yeah, for us, feedback has generally been very, very specific and high quality. ## What's the Plan for 2023 **What're you most excited for in the coming year?** Plain is now in a really cool place where we have moved on from the basics and are now getting really deep into some of the hard problems within customer support. For example, right now we're looking at managing triaging and SLA and complex queue management. It's going to bring with it, a whole host of other API integrations, to enable users to be in control of their support queues and prioritise some customer issues over others, and so on. I really can't wait to share our solution here. We're also going to be growing the team ([we're hiring!](https://plain-public.notion.site/Help-us-power-support-for-the-world-s-best-companies-7ea2f1a4cc084b96a95141a30e136b5b)) and onboard many more customers - it's going to be an exciting year for Plain! # api-key-management Source: https://speakeasy.com/blog/api-key-management You spend your time building the perfect API: the error messages are insightful, the external abstractions are flawless – but before anyone can use your beautiful API, they need the keys. That might mean someone spending a week hacking together a passable API key management system (it can't rotate keys, but we'll get to it next quarter). Or it could also mean the team manually sharing keys via OneTimeSecret with every client that needs onboarding. Neither scenario is great. Or you could let Speakeasy do it for you. We now offer a self-service API key management embed, which can be easily integrated with your API gateway. One less thing you need to worry about when you're launching your new API. ## New Features **API Key Management** - We're working on making the entire API user journey self-serve and that journey starts with enabling key management for your API users. With Speakeasy's key management embed, your users can create and revoke API keys in your developer experience portal. Set up is easy: Speakeasy integrates directly with your API gateway to externalize key management through an OpenAPI Spec extension; it works with any API Gateways that supports OIDC2 workflows and external token sources. This includes Google Cloud Endpoints, Kong, Apigee, AWS API Gateway and more. If there's a gateway not on this list that you'd like to use, reply to this email, or [come talk to us in Slack](https://go.speakeasy.com/slack). ## Incremental Improvements **Dark Mode** - Ensure your users have a consistent developer experience. Speakeasy's dev portal embeds now fit seamlessly into your existing developer portal. All our embeds now respect dark mode settings at the operating system and site level to make sure the developer experience in your portal is consistent, dark, and handsome. # api-landscape Source: https://speakeasy.com/blog/api-landscape Two weeks ago Postman published their [**API Platform Landscape**](https://blog.postman.com/2022-api-platform-landscape-trends-and-challenges/). They've done a great job highlighting some of the organizational challenges faced by companies building APIs. We at Speakeasy wanted to chip in with our own two cents. Specifically, we wanted to highlight the day-to-day challenges facing developers on the ground when they're building and managing APIs. Our thoughts have been shaped after spending a lot of time over the past 6 months talking to developers about their experiences building & managing APIs. To put some numbers behind it, my cofounder Simon and I have spent a combined 70 hours interviewing over 100 developers about their experiences. All of that goes into our work at Speakeasy, and in the spirit of openness, we wanted to share our learnings with everyone else. So, without further ado, here's what we have learned… ## Trends ### Developers want good UX We touched on this in an earlier post, but developers are increasingly demanding that the tools they use have a great UX. Tools should largely abstract away complexity without becoming a black box. This doesn't mean just throwing a GUI in front of the terminal. Developers want tools that are embedded in their existing workflows. This may mean that a tool intended for developers needs to have multiple surface areas. Features may exist as a CLI, VS Code extension, Git Action, Web app, React Embeds, API endpoint, etc. DevEx should prioritize helping the user accomplish their task with minimum friction and deviation from their existing workflows, and be flexible on where/how that feature is exposed to users. ### A “Design-first” approach has limits The “Design-first” approach to APIs is often presented as a virtue, to be contrasted with the dirty sin of a code-first approach (implementing the API without formal design). Most companies will claim they follow a purely design-first approach, however talking to developers on the ground, we found a much more nuanced reality. In fact, it's almost always a mix of the two. Many teams confessed that they felt writing the whole Open API spec was a tedious exercise for little gain. In their weaker moments they would often forgo the full open API standard in the beginning in order to get started faster, then they would go back and generate it later for the purpose of documentation. ### Different API frameworks for different jobs GraphQL, gRPC, Websockets, are all hot on the block, meanwhile RESTful still constitutes the vast majority of APIs (80+%). Although the twitter debates continue to rage and there will always be champions for different paradigms, we see things eventually settling into a comfortable coexistence. Websockets are great for real time applications, gRPC is great for microservice architectures, GraphQL is a great choice when you control both the client and the data source, and REST is great when you are/will be working with external consumers. We expect that in 5 years time it will be very normal for companies to be using all of the above as part of their technical architecture. ### API Platform teams are choosing to build on top of the best Thanks to a tight labor market and the Cambrian explosion in 3rd party dev tools, platform teams have switched tactics when it comes to how they support application developers. In the past, internal teams struggled with the sisyphean mandate of trying to handroll an entire scaffolding to support their companies development. We're seeing a new trend emerge, where lean platform orgs have the mandate to pick the best vendor products available, and stitch them together to create something tailored to their own business's needs. This helps to keep the company's resources focused on the creation of differentiated value for their customers. ## Challenges developers face ### API testing is still limited When it comes to exhaustive testing of an API, there can be a dizzying number of parameter permutations to consider. So, while it's great that developers have tools that let them easily mock an ad hoc API request, creation of a test suite for production APIs can be a major pain point. Similarly, test suites for internal microservices (understandably) don't get the same attention as endpoints used by external clients. However, oftentimes the external endpoints rely on a collection of microservices to work. It can be hard to effectively test the external endpoint if there's no corresponding test suite for the microservices it relies on. If behavior changes is it because something in the external endpoint changed, or one of the underlying microservices? ### API usage is hard to grok Which customers use this API? Can this old version be deprecated? Is anyone still using this parameter? All common questions developers ask, all unnecessarily difficult to answer. The lucky developers have Datadog dashboards and full sets of logs in warehouses. However, many developers lack this sort of reporting infrastructure. [**It's a big up front investment to set up, and an expensive ongoing cost**](https://www.linkedin.com/feed/update/urn:li:activity:6945789783235338240/). Small orgs can't justify the upfront time investment, and even larger orgs often go without for internal endpoints. The result is that developers lack an API-centric view where they could easily get a real understanding of how their API is being used in the wild. This makes it difficult for new developers to quickly grok how the API endpoints are used by consumers, how they work internally, and what to consider when working to evolve the API. ### APIs are stuck on v1 When companies roll out an API, they will confidently put v1 in the API path. This is to highlight that in the future, they will be evolving the API and rolling out a new version, v1.1 or maybe even a v2 someday. Often though, the API never moves past v1. Now, this could be because the developers who built the API were oracles with perfect foresight and the API has never needed to be changed. In that case, hats off. More commonly though, the APIs failure to ever evolve is a byproduct of a broken platform behind the scenes. In conversations we heard devs say over and over, “We know it's going to be painful down the road, we're trying not to think about it.” Without a robust platform providing scaffolding, versioning APIs is a near impossible task. Teams opt to live with the pain of never updating their APIs. In cases where the team gets really desperate, they may change the behavior of the v1 API and brace themselves for the storm of angry client emails. ### Every problem with APIs is exacerbated for internal APIs It's been 20 years since [**Jeff Bezos's (in)famous platformization memo**](https://chrislaing.net/blog/the-memo/). At this point, most software companies have formally adopted a microservice architecture mandate; there's no doubt that companies are chock-a-block with services. The problem is that microservices tend to get micro-resource allocation when it comes to their tooling. All the problems we discussed: grokking usage, exhaustive testing, service evolution, are exacerbated for internal APIs. This leads to stale documentation, unintended breaking changes, and unexpected behavior. Left to fester, it can create a culture where developers become reluctant to trust that a service works as described. They will spend time interfacing with internal teams to make sure that the service works as they expect. Loss of trust in reliability has real costs in terms of developer's productivity. ### Developers lack support from the wider org APIs are an inherently technical product. They are built by developers, and they are consumed by developers. It's therefore understandable why organizations have siloed all aspects of managing APIs to their development teams. But if you consider that APIs are a product line for many businesses, that is foolish. APIs require customer support, and product marketing and all the same resources as a conventional software product. When API development teams are left to handle all these aspects of product development on their own, they either: 1) do it poorly because it's not their expertise, or 2) don't have time to develop new features, because their time is sucked up by other management concerns. # api-linting-postman-generation Source: https://speakeasy.com/blog/api-linting-postman-generation import { Callout, ReactPlayer } from "@/lib/mdx/components"; Two exciting new features are coming to Speakeasy this week: API Linting and Postman Collection Generation. Let's get into it 👇
## API Linting Where does API developer experience start? SDKs are super important, but they're the final step in the integration process. The first step is the API itself and making sure that a consistent interface is being exposed. How do you ensure that your API is consistent, especially as the number of developers contributing grows? With an API linter of course! Now, linters don't have the greatest reputation in the developer community, but that bad reputation is deserved. Most linters have been a drag on developer productivity. They throw up opaque warnings and unnecessarily block progress on feature development. We saw an opportunity to do better. We've focused on making sure the Speakeasy linter will speed up development, rather than slows it down. Here's how our linter is different: - It runs where your work happens: - Speakeasy CLI - VS Code extension - CI/CD pipelines - The output is human-readable and clearly actionable - It comes with our robust default ruleset that can be easily customized with spectral rules to match your org's API style guide. To get started just update to the latest version of the Speakeasy CLI and run `speakeasy lint`. ## Postman Collection Generation [Alpha] Ready your curl requests, because Speakeasy is now able to generate high quality Postman Collections from your OpenAPI document. Postman Collections facilitate the easy adoption, development, and testing of your APIs. They allow users to understand the API's capabilities quickly through executing API requests without the need to set up a coding environment. As with any API artifact, a collection is only as useful as it is up-to-date. Speakeasy's Postman Collection generation is designed to be a seamless part of your API development workflow. Collection generation is free while it's in alpha, so give it a try and let us know what you think! ## 🚢 Improvements and Bug Fixes 🐛 Based on the most recent CLI version: [**Speakeasy v1.253.3**](https://github.com/speakeasy-api/speakeasy/releases/tag/v1.231.0) ### The Platform 🚢 Perf: Speed up validation runs by skipping name resolution ### Typescript 🧹 Chore: Remove some unused inbound zod types ### Terraform 🚢 Feat: Support mapping environment variables to provider configuration\ 🚢 Feat: Composite Import support\ 🚢 Feat: Generate import.sh snippets for additional terraform resource documentation\ 🚢 Feat: Support custom (non generated) resources and datasources being added into the provider\ 🐛 Fix: always instantiate required arrays to empty slices to avoid `null` being sent over the wire\ 🐛 Fix: extensions being ignored when under an `allOf` ### JavaV2 🚢 Feat: Better examples ### C# 🎉 Now in GA! 🎉\ 🚢 Feat: Retries\ 🚢 Feat: oneOf (union) support\ 🧹 Chore: Reduced HttpClient instantiation ### Python 🐛 Fix: after_error hook better error handling # api-ops-usage-monitoring Source: https://speakeasy.com/blog/api-ops-usage-monitoring When building & managing APIs, understanding usage and monitoring for changes is critical to success. The following post summarizes my experience working on LiveRamp's web APIs (2016-2018). I will go through the tooling & processes we used to diagnose and triage issues with our APIs (~6 billion requests/day), as well as the gaps in tooling I wished were filled. I hope that other people working on API operations find this useful, and share what has worked well for them. Before diving in, I'm fully aware that in software, citing experiences from 4 years ago is often an exercise in exploring the ancient past; pace of change and improvements typically render anything older than 2 years null and void. Given the way APIs and microservices have moved from the periphery to foundational components of the modern tech stack over the last 10 years, the expectation would be that the way APIs are built and managed would've correspondingly undergone significant change. And yet, the primary tooling for monitoring, detecting and troubleshooting APIs doesn't seem to have evolved much during my hiatus from the space. People are by and large still using the same set of tools as they were back in 2016. ## Overview of Tooling Our team was a laboratory for experimentation within the business, and consequently we had our hands on some of the best tools available. Consequently we got exposure to some of the most commonly used tools. When I began working with the team, our logging/monitoring setup was: - **Cloud Provider**: AWS - **Logging infrastructure**: Kinesis Firehose - **Data Warehouse**: Redshift/Athena/S3 - **Analysis & Insights**: Kibana & Datadog By the time I stopped working with the web APIs team, the setup was: - **Cloud Provider**: GCP - **Logging infrastructure**: Gazette ([**the precursor to Estuary's Flow**](https://docs.estuary.dev/)) - **Data Warehouse**: BigQuery - **Analysis & insights**: Grafana & Datadog Regardless of the stack used, the tools did what they were intended to do. More interesting to me is what the tooling couldn't do and the operational processes we developed to cope with that lack of tooling. ## Plugging the gaps with Process Logging infrastructure was great for alerting us that something was amiss, but diagnosing and triaging the issue was an APIOps activity that was largely manual. To help ourselves we developed processes to categorize issues, and then respond accordingly: ![Issues categories with impact, frequency, and description.](/assets/blog/api-ops-usage-monitoring/api-ops-usage-monitoring-image-01.png) Here's an overview of how we would diagnose issues… ![A diagram of how issues are resolved.](/assets/blog/api-ops-usage-monitoring/api-ops-usage-monitoring-image-02.png) Rather than beat a dead horse by reiterating the exact text in the flow chart, I want to dive into dealing with breaking change issues. I will give some specific advice for proactively mitigating these types of issues, what the challenges are and what the gap in tooling is. ### Breaking change to an upstream dependency - **Note:** In the microservices world we're all living in, it's very likely that an external service relies on a dozen or more internal microservices to function. The typical software business might have hundreds or thousands of microservices. For most, it's not realistic to invest in the same level of tooling as they do for external API endpoints (logging, dashboarding). It's therefore entirely possible, dare I say likely, that a microservice team is unaware that external APIs are dependent on their service. - **How to diagnose**: All that in mind, these can be tricky to diagnose. The first sign will be an anomaly in your traffic patterns. Figure out when the anomaly first appeared, get as specific a time if you can (hopefully you'll be able to find a step change). First check to make sure that the anomaly doesn't line up with a push by your own team. Next you need to figure out if an upstream dependency is the cause. - **How to triage**: You could take time to dive into the code to test dependencies, but for the sake of speed, I recommend making a list of the teams responsible for the microservices your team depends on. Reach out to each team to check if any of them made a change around the time the anomaly appeared. If there's a match, ask them to rollback their change so you can see whether the rollback fixes the problem. After client impact has been mitigated, take the time to dive in and figure out where/how the dependency broke. - **Advice**: For improved speed, I recommend maintaining a list of the dependencies, and the owning teaming for each. This will allow you to move faster when it matters most. As I mentioned above, a lot of team's lack the tooling that would be required to mitigate issues like this. ### Breaking change to the API itself - **Note:** Breaking changes in themselves are not necessarily a problem if you are properly versioning your API ([**Brandur Leach wrote a great piece on versioning**](https://stripe.com/blog/api-versioning)). In this case, I'm referring to accidental breaking changes, which are definitely a problem. - **How to diagnose**: Fastest way to spot them is if you notice a spike in error codes and it corresponds with a release your team made to production. Unfortunately, not all breaking changes trigger spikes in errors. The problem may only manifest when two API calls are used in sequence, see if there's any kind of unexpected change in the overall traffic patterns (e.g. drop in total calls to an endpoint). - **How to triage**: If an anomaly in the data corresponds with a push to production, then rollback the change ASAP. Even if you haven't diagnosed the exact error, the priority is client stability. Later once you have made a diagnosis, address the issue, and test very carefully before pushing to prod again. You will owe your customers an explanation, write out what caused the error, and the steps the team is taking to make sure the issue never reoccurs. - **Advice**: Again this is a hard one. If you're not doing versioning, then do versioning before your client's leave you. Integration testing is also definitely your friend. Similar to upstream breaking changes, feels like there's a tooling gap here that could be better addressed (discussed below). ## The Gaps as I see it As I said earlier, it strikes me that the development of these processes was in order to cope with a lack of tooling. There are a few things that it would be great to have, which aren't available today. My hope is that in time Speakeasy is able to help devs make steps in addressing some of these gaps. 1. **API-centric monitoring:** The entire process for troubleshooting I layed out is dependent on making a not insignificant investment in logging and dashboarding infrastructure. Without these, understanding usage is difficult and the surfacing of issues is likely to come in the form of an angry email from your client. It's always struck me that we were extremely fortunate to have been able to make that investment in a robust logging infrastructure, complete with relevant dashboards and automated alerting. At Speakeasy we believe there should be something more API-native and developer-first. Devs should be able to drop-in a couple lines of code and get something that works out of the box. This would provide smaller teams the same level of insight into usage as large companies. Who's using which API version, is the integration healthy, are there anomalies? 2. **API Developer Guardrails:** Most unintentional breaking changes follow similar patterns: adding a required field, changing the response structure, changing a method or resource name, etc. Introducing some guardrails that could sense check changes for common mistakes would go a long way towards helping devs avoid costly client errors. It's something that a lot of big companies have, that is often too expensive for a smaller org to develop internally. 3. **API Dependency Mapping:** I mentioned how it can be useful to maintain a list of the dependencies an API depends on. That's really a stop gap measure. It would be better if this could be automatically tracked, and what would be even better is if there was tooling which made it easy for microservices to understand the usage of their services, so they could track use by external services. I'm really curious about how much overlap there is between my experience and those of other people. Would love to hear what off-the-shelf tools other people have used to manage their API operations, and what processes people have developed internally to cope with the lack of tooling. # apis-for-global-shipping Source: https://speakeasy.com/blog/apis-for-global-shipping ### TL;DR - Platformization efforts can start off as a single team mandate, but you need to figure out how to scale them into an org wide mandate to be successful. - For larger companies with an acquisition strategy, an API platform can be the difference between a successful or a failed integration with an acquired business. - Public API teams often evolve into having a DevEx mandate. Making users successful in using your API isn't narrowly focused on documentation. - For a good DevEx, focus on eliminating the need for any interaction with customer support. ## Introduction **_Could you explain what you & your team are responsible for?_** I'm a Senior Engineering Manager of Flexport's Public API team, which also goes by the moniker of ‘Developer Platform'. Those two things are related because the API platform serves as the foundational piece of our external developer experience. We're a 2.5-year-old team, and we're looking to greatly expand our scope to become the horizontal tooling provider for all the internal Flexport engineering teams who publish and maintain APIs, and enable them to give external non-Flexport developers a superior industry-leading experience. [**On flexport.com, if you open the “Developers” menu item**](https://developers.flexport.com/s/?_ga=2.155367484.687997278.1658178205-206804922.1658178205) – everything listed there is built by, or supported by my team in partnership with teams across our entire Technology organization. ## APIs At Flexport **_What are the key goals for the API platform team?_** A key goal for the company is to become more API-first – as our Executive leadership has said, “The vision is to build the technology platform to make global trade easier. We will open up more and more capabilities for anybody in the world to use”; [**we are building the API platform for internal developers, partner developers, as well as 3rd party developers**](https://www.freightwaves.com/news/flexport-to-open-platform-for-third-party-app-developers). That's the strategic product vision my team seeks to achieve, so internal and external application developers can build on Flexport's tech platform. I think this story will sound familiar to many companies: APIs will only increase in importance as a lever for Flexport's future growth and scale. A key goal for my team is to increase end-to-end automation for internal and external API developers, which will, in turn, increase API creation and adoption. We're always thinking about ways that we can empower these engineering teams to expose their services externally, as easily and quickly as possible – while giving them access to the critical functionality needed to run a great API. That means taking care of everything from observability, telemetry, monitoring, testing, scalability and reliability, through to hosting developer portals, documentation, developer tutorials, code samples and OpenAPI schema creation. **_What is the value of having the API Platform?_** It's all about increasing efficiency and throughput for internal teams, our customers and ultimately growing the business. Flexport constantly launches new product lines; APIs are critical for letting teams build products quickly and allowing partner developers to create engaging apps to delight our customers. Our API platform enables the internal teams to develop and maintain APIs faster, and deliver the consistency external developers deserve. It's worth mentioning that, because we are a large company, Flexport does make acquisitions and strategic partnerships with other tech companies. When we're making an acquisition or building a new partnership, our APIs facilitate the integration and a frictionless customer journey. **_You mentioned before that Flexport is ‘becoming' API-first. Can you tell us about what Flexport is evolving from?_** Flexport is a company that was founded in 2013. We started off delivering much of our service via web apps, though our public API usage has steadily increased over time. As our customer-base has grown, we've added more clients and partners who require enterprise-level platform integrations via API and want to embed Flexport functionality in their own software applications, which is why APIs have become a major focus as we seek to aggressively automate all aspects of the vast global supply chain which includes many operational tasks from origin to destination. To seed the process of evolving from the monolith to a more flexible SoA/microservice architecture, API ownership was driven by my team as a consolidated initiative. That's why my team directly maintains a portion of Flexport's public APIs. That was good for getting started, though we sought out a more scalable solution for the long-term. APIs are merely a different interface for accessing the same set of entities and operations that Flexport's web apps and internal APIs offer; therefore, each product team should own their corresponding APIs in a federated model. We're nearly done with this transition; each individual product team is starting to own the REST APIs in their vertical domain. Going forward, my team's role will be to focus more on their enablement, maintaining consistent REST patterns, observability, automation, testing, monitoring and providing a world-class experience to our internal and external devs who call our public API; though when business priorities necessitate, we can still quickly mobilize our resources to directly implement new APIs on behalf of domain teams to help accelerate delivery. Our goal is public API feature parity with the functionality available in our web apps. Flexport's own application and platform integration developers are major consumers of our public APIs currently and we will continue to build and launch customer applications on top of our public APIs; there are a number of exciting apps and new APIs in various stages of development that our customers are going to love. **_What type of tooling has your team rolled out to the teams at Flexport_** Yeah, good question. Networking and security optimizations were some of the first things we tackled. Then we partnered with other teams to standardize infrastructure, identity management, and so forth. Next, we focused on building a comprehensive API best practice guide: from the networking layer, down to what the routes look like along with modeling new personas. We want to make sure the look and feel of Flexport's APIs reflect REST standards. We also developed opinionated guidance about things like pagination, sorting, filtering, resource structure, URI, versioning and Documentation. We've launched Flexport's [**Developer Portal**](https://developers.flexport.com), in addition to our [**API reference**](https://apidocs.flexport.com). So now, these templates and guidance are all documented for Flexport teams, and we are turning our attention to making implementation as turnkey as possible. Self-service is our northstar; both for the internal Flexport developer publishing and consuming APIs, and also, for the external developers we serve. We have rolled out 24x7 public API service health monitoring with our [**API status page**](https://status.flexport.com) and are proud to consistently exceed our uptime availability target. ## Developer Experience **_You mentioned your team has a larger DevEx mandate. How did you grow from API ownership to DevEx ownership?_** This is a pattern I've seen at other companies I've worked at as well. It's common to start with a public API enablement team that has a fairly broad charter. At my previous job, we were building a lot of those frameworks and platforms for the businesses, which the different business units and teams could all leverage, and the end goal is always to make sure that the APIs have a cohesive customer experience across all the different product lines. And then that goal naturally expands from cohesive API experience to cohesive developer experience across the whole external surface. **_What do you think constitutes a great developer experience?_** I've been professionally working on public API and developer platform enablement / DevEx for almost a decade. I've collaborated with a lot of developer advocates and partnered with many folks building incredible industry-leading developer platforms. One of the things that is essential to a great developer experience is frictionless self-service. In a given year you should not need to talk to a human being via phone or email in order to successfully build applications on a strong technology platform. You only have one chance to make that positive first impression. And if you say, ‘Hey, you must talk to a human being to get your account verified', most developers won't come back, especially if you're not really a well-known entity. I'd also avoid having a paywall. There are metrics that show that having a paywall in front of an API will cause your developer conversion to drop significantly. I recommend using a rate-limited free tier for developers to use in order to increase adoption. Another part of self-service is the documentation. [**You need to have very good and accurate documentation for developers**](https://apidocs.flexport.com/). You should provide code samples, SDKs, example applications, and [**status pages**](https://status.flexport.com/). My opinion is that the best you can provide is a sandbox for developers to actually interact with live. But you should make an effort to provide documentation in various formats. Written first and foremost, but some people respond better to video tutorials. We want to provide API consumers self-service access to metrics and usage dashboards, logs, errors and other configurations across production and non-production environments. Lastly, you want to make sure the surface area is consistent and follows standards. For me personally, when I start using an API and I can see that they haven't implemented best practices, I will question whether the company is reliable. For example, if the API isn't rate-limited then that makes me think that they don't really think things through. So, make sure you are embracing those best practices from the beginning. ## Improving APIs **_What are some of the KPIs your team tracks against?_** Of course we look at uptime, since availability of our services is table stakes. We then look at active users over a given timeframe, usually monthly or weekly for our business. We track the active users (MAUs), latency (p99, p95, p90, etc.), volume of requests, uptime availability and error rate for each endpoint, and also the emitted web hooks. Those are the most basic metrics that are universal across the teams. Every team may have its own additional KPIs that they care about as well depending on what their team's specific business objectives are. **_What are the main challenges facing developers building public APIs at Flexport?_** Our public APIs are not quite at feature parity with our internal capabilities. Our push to automate API operations will help improve our API development velocity as we strive for public API feature parity. On that topic, a lot of our tooling and processes around APIs are still more manual – multiple manual steps, reviews and synchronous activities are required to get an API exposed externally. That's what a developer would mention as the primary opportunity. For my team, bringing automation to many different concerns, across such a large surface area of APIs is definitely a huge opportunity. Another topic we are navigating is better defining the responsibilities of platform teams like mine vs. the domain teams that build the API business logic. Today it can be fuzzy, though for the most part ownership is clear. Who is responsible for latency, performance monitoring and load testing? How do we help domain teams even be aware of performance concerns and become more sensitive to it? Customer delight is our primary goal and we drive towards this relentlessly. **_How about a developer integrating with Flexport APIs? What are their main challenges?_** We frequently receive requests for enhancements to our API and developer platform experience, and due to where we are in our API journey, we get a lot of requests from customers to expose more functionality via our public API. As I said before, our APIs aren't at parity with our web app interfaces and internal APIs yet. So that's definitely the most common request, to expose more internal functionality via our public APIs; to accomplish this we need to work broadly across our Tech org. ## API Architecture **_If someone was designing their API architecture today, what advice would you give them? How would you approach using REST or GraphQL?_** Yes, this is a good question, and one I've been asked ever since I joined Flexport and started managing APIs. What I would say is that there is no right answer, you need to build with your customer in mind to delight the customer and deliver high-value business impact. At Flexport we are working in freight forwarding; we're the leading technology disruptor in the supply chain logistics space. While the sector is digitizing, the rate of B2B digitization may be slower than some B2C industries, say Finance & Fintech. Many of our partners in the space have been operating for much longer than Flexport. We do not have customers asking us for public GraphQL right now. That may happen in the future, and if there was a compelling customer use case we would consider it, though for our current customers and partners, REST endpoints and webhooks satisfy their requirements. If you're in a space that is catering to developers who are asking for it, GraphQL might be worth considering. At my previous company we had some GraphQL public APIs though the customer demand was overwhelmingly for REST. ## Closing **_A question we like to ask everyone: any new technologies or tools that you're particularly excited by?_** I'm curious about AR and VR. We have held a couple of hackathons and for one of those hackathons, I built a VR treasure hunting game. Flexport is a mix of people; some of us are from the logistics, supply chain and freight industry, while others have not actually worked in this domain. There are people at Flexport who have never had the opportunity to visit a working port, or been on a container ship. So I built a little VR game in 2 days so that people could visually explore those different locations. In the game, you are on the hunt for personal protective equipment (PPE) aboard container ships, since during that hackathon, we were at the beginning of the COVID-19 pandemic and Flexport via [**Flexport.org**](https://www.flexport.org/) was making a big push to [**ship PPE to frontline responders**](https://www.flexport.com/blog/the-drive-to-mobilize-ppe-flexport-raises-usd7-9m-to-move-medical-supplies/) where we raised over $8 million. You can play the game at [**eesee.github.io/justflexit**](https://eesee.github.io/justflexit/) # APIs vs. SDKs: Understanding the Differences and Practical Applications Source: https://speakeasy.com/blog/apis-vs-sdks-difference In the interconnected world of modern software, APIs (Application Programming Interfaces) and SDKs (Software Development Kits) are indispensable tools. APIs act as the bridges that allow different applications to communicate and share data, while SDKs provide developers with the toolkits they need to build upon these APIs efficiently. Choosing whether to use an API directly or leverage an SDK is a crucial decision that can significantly impact a project's timeline and overall success. This guide will clarify the distinctions between APIs and SDKs, explore their common use cases, and outline best practices for both. --- ## Quick-Reference Summary Here's a brief table that highlights the fundamental differences between APIs and SDKs at a glance: | **Aspect** | **API (Application Programming Interface)** | **SDK (Software Development Kit)** | |--------------------------|-----------------------------------------------------------------------------------|----------------------------------------------------------------------------| | **Definition** | A set of rules and protocols for communication between software components | A bundle of tools, libraries, and documentation to accelerate development | | **Scope** | Focuses on how to send and receive data (often via HTTP/HTTPS) | Provides prebuilt code, testing frameworks, and platform-specific support | | **Implementation Detail**| Requires developers to handle requests, responses, and error handling manually | Abstracts complexities with prewritten methods and classes | | **Platform Dependency** | Typically platform- and language-agnostic (REST, GraphQL, gRPC, etc.) | Often tied to a specific language or ecosystem (Android SDK, iOS SDK, etc.)| | **Use Case** | Ideal for lightweight integration, direct control, or cross-platform scenarios | Best for rapid development, built-in best practices, and platform-specific features | --- ## What Are APIs? An **Application Programming Interface (API)** is a set of rules, protocols, and definitions that enable different software components to communicate. It acts as a “contract,” specifying how requests and data exchanges occur between systems, such as a client application and a remote server. APIs serve as fundamental building blocks in modern software. They allow developers to leverage sophisticated services (e.g., payment gateways, location services) without building them from scratch. Internally, APIs make it easier for teams to create modular, scalable applications by standardizing communication between different components and services. ### Popular API Approaches - **REST (Representational State Transfer):** REST is the most widely used approach for creating APIs, primarily due to its simplicity and compatibility with HTTP. It dictates structured access to resources via well-known CRUD (Create/Read/Update/Delete) patterns. A common pattern in modern web development is to create a front-end written in React or a similar framework, which fetches data from and communicates with a back-end server via a REST API. - **GraphQL:** GraphQL is a newer API technology that enables API consumers to request only the data they need. This reduces bandwidth required and improves performance, and is particularly suitable in situations where a REST API returns large amounts of unnecessary data. However, GraphQL is more complex to implement and maintain, and users need to have a deeper understanding of the underlying data models and relationships in order to construct the right queries. - **gRPC (Google Remote Procedure Call):** gRPC is a high-performance, open-source framework designed for low-latency and highly-scalable communication between microservices. gRPC is strongly-typed, which helps catch errors earlier in the development process and improves reliability. However, gRPC ideally requires support for HTTP/2 and protocol buffers, which many web and mobile clients may not support natively. Also note that far fewer developers are familiar with gRPC than REST, which can limit adoption. For these reasons, gRPC is mainly used for internal microservice communications. In summary, REST remains the most popular API technology due to its simplicity and widespread adoption. GraphQL and gRPC are popular for specific use cases. --- ## What Are SDKs? A **Software Development Kit (SDK)** is a comprehensive collection of tools, libraries, documentation, and code samples that streamline application development on a specific platform or for a specific service. While an API defines how to interact with a service, an SDK provides ready-made resources to speed up that interaction. Key components of SDKs include: - **Pre-Written Libraries**: Reduce boilerplate by offering out-of-the-box methods and classes - **Development Utilities**: Provide testing frameworks and debugging tools - **Platform-Specific Resources**: Include documentation, guides, and environment setup instructions For example, the **Android SDK** includes compilers, emulators, libraries, and tutorials, allowing developers to build Android apps with minimal friction. --- ### Why Do SDKs Add Value to API Integrations? Without an SDK, you must manually handle HTTP requests, parse responses, implement error handling, manage authentication, and maintain the correct sequence of API calls. SDKs solve many of these pain points by: - **Development Efficiency**: Simplify method calls (e.g., `client.placeOrder(...)` instead of manually constructing endpoints and payloads). - **Type Safety & Consistency**: Strongly-typed interfaces reduce integration errors. - **Maintenance Benefits**: Common patterns and best practices are baked into the libraries. - **Change Management**: Many SDKs transparently handle minor API updates under the hood. --- ## How Do APIs Compare With SDKs in Practice? ### Example: Direct API Integration To highlight these differences, let's look at an example of what integrating with an e-commerce API might look like, first without an SDK and then with one. The use case will be enabling a new customer to place an order. This requires fetching information about the product being ordered, creating a new customer, and creating the order itself. **First, here's what integrating might look like without an SDK:** ```typescript const fetch = require('node-fetch'); const apiKey = 'your_api_key'; const baseUrl = 'https://api.ecommerce.com/v1'; const headers = { 'Authorization': `Bearer ${apiKey}`, 'Content-Type': 'application/json' }; const productName = 'Awesome Widget'; const customer = { firstName: 'John', lastName: 'Doe', email: 'john.doe@example.com' }; const quantity = 2; async function placeOrder(productName, customer, quantity) { try { // Step 1: Get product information const productResponse = await fetch(`${baseUrl}/products`, { headers }); if (productResponse.status !== 200) { throw new Error(`Could not fetch products. Status code: ${productResponse.status}`); } const productData = await productResponse.json(); const product = productData.products.find(p => p.name === productName); if (!product) { throw new Error(`Product '${productName}' not found.`); } // Step 2: Create a new customer const customerResponse = await fetch(`${baseUrl}/customers`, { method: 'POST', headers, body: JSON.stringify({ customer }) }); if (customerResponse.status !== 201) { throw new Error(`Could not create customer. Status code: ${customerResponse.status}`); } const customerData = await customerResponse.json(); const customerId = customerData.customer.id; // Step 3: Place the order const orderResponse = await fetch(`${baseUrl}/orders`, { method: 'POST', headers, body: JSON.stringify({ order: { customerId, items: [ { productId: product.id, quantity } ] } }) }); if (orderResponse.status !== 201) { throw new Error(`Could not place order. Status code: ${orderResponse.status}`); } console.log('Order placed successfully!'); } catch (error) { console.error(`Error: ${error.message}`); } } placeOrder(productName, customer, quantity); ``` Note that the API consumer would need to construct all this code themself. They would need to refer to the API documentation to figure out which APIs should be called, what the response data structures look like, which data needs to be extracted, how to handle auth, what error cases might arise and how to handle them. **What You Manage Manually:** - Constructing requests and headers - Parsing responses - Handling errors for each call - Managing authentication - Sequencing the calls to ensure proper workflow **Now here's the SDK version of this code. Using an SDK, the same functionality can be achieved with much greater ease:** ```typescript const { EcommerceClient } = require('ecommerce-sdk'); const apiKey = 'your_api_key'; const client = new EcommerceClient(apiKey); const productName = 'Awesome Widget'; const customer = { firstName: 'John', lastName: 'Doe', email: 'john.doe@example.com' }; const quantity = 2; async function placeOrder(productName, customer, quantity) { try { await client.placeOrder(productName, customer, quantity); console.log('Order placed successfully!'); } catch (error) { console.error(`Error: ${error.message}`); } } placeOrder(productName, customer, quantity); ``` Notice how much simpler and concise it is. Authentication is handled automatically with the developer just needing to copy in their key. Pre-built functions mean the developer doesn't need to parse through pages of API docs to stitch together the required calls and associated data extraction themselves. Error handling and retries are built-in. Overall, a far easier and superior experience. **Advantages of Using an SDK:** - **Dramatically Reduced Code Complexity**: Fewer lines of code and clearer logic flow - **Automatic Authentication and Error Handling**: The SDK's internal routines handle retries, rate limits, and token refreshes - **Built-in Best Practices**: Consistent data structures and naming conventions - **Faster Onboarding**: Less time spent referencing raw API docs --- ## What's the difference between SDKs and APIs? APIs and SDKs serve distinct yet complementary roles in software development. **APIs** provide the underlying communication protocols and offer broad flexibility, while **SDKs** wrap these protocols with ready-to-use libraries and best practices that make development faster and more consistent. In summary, APIs & SDKs are symbiotic. Let's talk about coffee to draw the analogy better. You can think of APIs as the fundamental, bare metal interfaces that enable applications or services to communicate. In our analogous example, APIs are like going to a coffee shop and getting a bag of beans, a grinder, a scale, filter paper, a coffemaker/brewer, kettle, and an instruction guide. Good luck making a delicious brew! SDKs on the other hand are critical to enabling APIs to reach their full potential, by providing a rapid, ergonomic way to access the API's underlying functionality. In our coffee example, SDKs are more akin to telling a skilled barista “I'd like a latte please”. The barista does all of the work of assembling the ingredients, and you get to focus on the end result. ## API and SDK best practices Now we know what APIs and SDKs do, what should you keep in mind as you're building them, to ensure they fulfill the promises we've outlined above? Here are some “gotchas!” to watch out for when building awesome APIs: - **Design carefully:** It can be extremely difficult to get users to change how they use an API once it's in production. Avoiding unnecessary breaking changes, where possible, will save you many headaches and irate users later. - **Documentation:** In addition to an “API reference” that details every endpoint and response, consider creating a “usage guide” that walks users through how to use APIs in sequence to accomplish certain tasks. - **Authentication:** Creating and sending users API keys manually works fine for an MVP, but has obvious security and scalability challenges. An ideal solution is to offer a self-service experience where end-users can generate and revoke keys themselves. For more on API auth, [check out our guide](/post/api-auth-guide). - **Troubleshooting and support:** Users will inevitably run into issues. It's easy for members of the team to quickly get inundated with support requests. Try to provide self-service tools for troubleshooting API issues, such as logging and monitoring, and community support channels. Building great SDKs presents a different set of considerations. Keep these in mind if you want to offer a great SDK to your users: - **How stable is the underlying API?** If the API is undergoing frequent changes, it might be particularly challenging to manually keep the SDKs up-to-date and in sync with the API. - **Creation and maintenance cost:** Creating native language SDKs for all your customers' preferred languages can be a huge hiring and skills challenge. Each language SDK also has to be updated every time the API changes – ideally in lockstep to avoid the SDK and API being out of sync. This is time-consuming and costly. Many companies have deprecated or scaled back their SDKs after misjudging the work required. - **Testing and validation:** Plan for thorough testing of the SDKs across different platforms and languages, including unit tests, integration tests, and end-to-end tests, to ensure the SDKs are reliable and compatible with the API. - **Documentation:** Provide clear examples and code snippets in each language to make the SDKs easy to use and understand. --- ## Simplify SDK Generation with Speakeasy While the benefits of providing SDKs are clear, creating and maintaining them across multiple languages can be a significant undertaking. It requires specialized skills, substantial development time, and ongoing effort to keep SDKs in sync with API changes. This is where Speakeasy comes in. Speakeasy is a platform that **automatically generates high-quality, idiomatic SDKs** from your API specification. Our solution helps you: * **Reduce Development Time and Costs:** Eliminate the need to manually write and maintain SDKs. Speakeasy handles the heavy lifting, freeing up your team to focus on core product development. * **Ensure SDK Quality and Consistency:** Our generated SDKs are built to follow industry best practices. Speakeasy offers comprehensive, automated testing, ensuring reliability and a consistent developer experience across all supported languages. Each generated SDK comes with: * **Comprehensive Test Coverage:** We provide a wide range of tests, including unit, integration, and end-to-end tests, to validate every aspect of the SDK's functionality. * **Automated Test Execution:** Our platform automatically runs these tests whenever your API specification changes, providing immediate feedback on any potential issues. * **Keep SDKs in Sync with API Changes:** Speakeasy automatically regenerates your SDKs whenever your API specification is updated, guaranteeing that your SDKs are always up-to-date. * **Improve Developer Experience:** Provide developers with easy-to-use, well-documented SDKs that accelerate integration and enhance their overall experience. Each generated SDK comes with extensive, ready-to-publish documentation: * **Interactive Code Examples:** Developers can see real code examples in their preferred language, making it easier to get started. * **Clear and Concise Explanations:** Our documentation is designed to be easy to understand, even for complex API interactions. * **Automatically Updated:** Documentation is regenerated alongside the SDKs, ensuring consistency and accuracy. * **API Insights:** Speakeasy provides detailed insights into your API's usage and performance. Our platform helps you track key metrics, identify areas for improvement, and ensure the reliability of your API. **How it Works:** 1. **Provide Your API Specification:** Share your OpenAPI or other supported API specification with Speakeasy. 2. **Configure Your SDKs:** Select the languages you want to support and customize the look and feel of your SDKs, including configuring authentication methods. 3. **Generate and Publish:** Speakeasy automatically generates your SDKs, runs comprehensive tests, creates detailed documentation, and makes them available for download or through package managers. Stop spending valuable time and resources on manual SDK development. Let Speakeasy handle the complexities of SDK generation, testing, and documentation so you can focus on building great APIs and delivering exceptional developer experiences. [Learn more about Speakeasy's SDK generation platform](https://www.speakeasy.com/docs/introduction). **Get started today, [book a demo with us](https://www.speakeasy.com/book-demo).** # auth-for-embedded-react-components Source: https://speakeasy.com/blog/auth-for-embedded-react-components ## Use case & requirements More and more developer products are being delivered as react embeds. We recently built out our infrastructure to support delivery of Speakeasy as a series of embedded react components. Why embedded components? The embedded components enable our customers to include Speakeasy functionality in their existing developer portals, surfacing their API request data to their users. Because our embeds are exposing API data to our customers' users, authentication is a critically important feature to get right. In this blog post I will walk through how we decided to implement authentication for our embedded react components, in the hope it helps someone else building react embeds that handle data. The elevator-pitch for this feature is: our users should be able to safely embed _our_ widgets into _their_ web applications. The list of requirements for this feature looks something like this: 1. **Data Segmentation:** Because our customers' web applications may be user-facing, our customers need to be able to dictate _what_ portion of the data gets rendered into the components. 2. **Manageable:** Customers need to be able to restrict access to someone who was previously authenticated. 3. **Read Only:** Embeds might be exposed to people who shouldn't be able to modify the existing state (upload an OpenAPI schema or change API labels, for example), so they should have restricted permissions. The **TL;DR** is that we evaluated two authentication options: API Keys, and Access Tokens. We ended up going with access tokens. Although they were more work to implement we felt they provided superior flexibility. Read on for the details of the discussion. ## How to Solve - Option 1: API Key? At the point that we were initially discussing this feature, we already had the concept of an api-key, which permits programmatic access to our API. An obvious first question is whether or not we could re-use that functionality to authenticate users of the embedded resources. Let's imagine what that might look like in the use case of our react embeds. ![Speakeasy react embeds example.](/assets/blog/auth-for-embedded-react-components/auth-for-embedded-react-components-image-01.png) That was easy! But does it meet our requirements? 1. **Data Segmentation:** Maybe, but it would take some work. We'd need to have some means of relating an api-key with a customer, but that's not too bad. 2. **Manageable:** Again, maybe. What happens when an api key needs to be revoked: unless a separate api key is produced for each user (imagine that one customer has multiple people using the embedded component), everyone using the newly-revoked token loses access. If a separate api key _is_ produced for each user, then the api-key becomes state that needs to be held, somewhere, and since Speakeasy isn't a CRM, that's now a table in _your_ database that _you_ need to manage. 3. **Read Only:** Not yet. We'd need to add some functionality to the api keys to distinguish between read-only and write-capable api keys. ### How does this live up to our requirements? Could work, but would require some work to meet the requirements. Maybe we can do better. ## How to Solve - Option 2: Access Token What would a separate “embed access token” implementation look like? Let's take a look at what tools we've already built that we might take advantage of. - We know that our sdk is in your API - We know we have the concept of a customer ID - We also know that you know how we're identifying your customers (you assigned the customerID to each request) - We know that you probably already have an auth solution that works for you. From the looks of it, we don't really need to understand who your users are or how you assign customerIDs to requests, and chances are, you don't want another service in which to manage your customers, because you probably have that logic somewhere already. Since the authentication logic _for_ your API is probably reliably available _from within_ your API, we just need to make it easy for you to give that authentication result to us. This is where we can take advantage of the Speakeasy server-side sdk that is already integrated into your API code, because it's authenticated with the Speakeasy API by means of the api key (which is workspace specific). ```typescript // Endpoint in api server with speakeasy sdk integration controller.get("/embed-auth", (req, res) => { const authToken = req.headers["x-auth"]; const user = userService.getByAuthHeader(authToken); const filters = [{ key: "customer_id", operator: "=", value: user.companyId }]; const accessToken = speakeasy.getEmbedAccessToken(filters) res.end(JSON.stringify({ access_token: accessToken})); }); ``` That takes care of understanding _how_ to segment the data, but how do we actually make that work? There are myriad reasons that you might want to control the scope of data exposed to your customers. We already have a filtering system for the Speakeasy Request Viewer. If we build the access token as a JWT, we can bake those filters _into_ the JWT so that they cannot be modified by the end-user, and we can coalesce the filters from the JWT and the URL, maintaining the existing functionality of the Speakeasy Request Viewer filtering system. Putting this all together, the resulting flow looks like: 1. You authenticate the user 2. You translate the result of authentication into a series of filters 3. You pass the filters into some method exposed by the Speakeasy sdk 4. We encode those filters into a JWT on our server 5. You return that JWT to your React application. ### How does this live up to our requirements? 1. **Data Segmentation:** Yeah, beyond even just by customer ID. 2. **Manageable:** JWTs are intentionally ephemeral. We already have logic to refresh JWTs for our Saas platform that we can re-use for the embed access tokens. 3. **Read Only:** This can be implemented with an additional JWT claim alongside the filters. ### Loose ends Requiring a new endpoint in your API to get the embedded components (option 2) working _is_ more work (potentially split over teams) than api-key based authentication. The consequence is that the endpoint has to be deployed _before the embedded component can even be tested._ To ameliorate this disadvantage, we added a **Create Access Token** button directly in our webapp, which generates a short-lived access token that can be hard-coded in the react code for debugging, testing, or previewing. ![Create embed tokens direclty in the webapp.](/assets/blog/auth-for-embedded-react-components/auth-for-embedded-react-components-image-02.png) ## Final Conclusion Access Tokens take a little more setup, but it's considerably more flexible than the original api key idea. It also works with whatever authentication flow you already have, whether it's through a cloud provider or your hand-rolled auth service. Additionally, because JWTs are ephemeral and can't be modified, this solution is more secure than the api-key method, which would require manual work to revoke, whereas the moment that a user can't authenticate using your existing authentication, they can no longer authenticate with a Speakeasy embedded component. # automate-oauth-flows-streamline-publishing Source: https://speakeasy.com/blog/automate-oauth-flows-streamline-publishing import { Callout } from "@/lib/mdx/components"; This week, we're excited to announce two major improvements to the Speakeasy platform: enhanced OAuth support and a more flexible publishing workflow. These updates are designed to make your API integration process smoother and more efficient than ever before. ## OAuth Authorization Flow Support with Custom Security Schemes ```typescript import { SDK } from "SDK"; const sdk = new SDK(); async function run() { const result = await sdk.oAuth2.getToken( { Id: process.env["SDK_ID"] ?? "", Secret: process.env["SDK_SECRET"] ?? "", }, { grantType: "authorization_code", code: "1234567890", redirectUri: "https://example.com/oauth/callback", }, ); // Handle the result console.log(result); } run(); ``` We've introduced robust support for OAuth authorization flows using custom security schemes. This new feature allows for greater flexibility in implementing various OAuth flows, particularly the Authorization Code flow. Key improvements include: - Custom Security Schemes: Define your own security scheme to match your specific OAuth implementation. - Flexible Secret Handling: Support for various formats of client ID and secret combinations. - Pre-Request Hooks: Customize request headers and parameters before they're sent to the server. This enhancement makes it easier than ever to integrate OAuth-protected APIs into your projects, with the SDK handling the complexities of token exchanges and header generation. --- ## Streamlined Publishing Workflow ![Manually trigger publishing](/assets/blog/publishing.png) We've completely revamped our publishing workflow to give you more control and flexibility. Now, you can publish your SDK without being tied to GitHub-specific generation processes. Here's what's new: - **Decoupled from GitHub Actions**: Publish directly from your branch, regardless of where the last generation occurred (local, GitHub, etc.). - **Simplified First-Time Publishing**: Follow a straightforward process: `quickstart` → configure GitHub → push to repo → kick off publishing. - No-Op for Existing Versions: If you attempt to publish a version that's already been released, the system will automatically skip the process, preventing accidental overwrites. This update eliminates the need to regenerate in GitHub Actions or worry about forcing changes. It's now easier than ever to get your SDK published and into the hands of your users. --- ## 🐝 New features and bug fixes 🐛 Based on the most recent CLI version: [**Speakeasy v1.418.4**](https://github.com/speakeasy-api/speakeasy/releases/tag/v1.418.4) ### TypeScript 🐛 Fix: error handling in SSE methods \ 🐛 Fix: support for gracefully aborting SSE streams ### Python 🐝 Feat: export `__title__`, `__version__` and `VERSION` values from the root of a python package \ 🐛 Fix: error handling in SSE methods \ 🐛 Fix: support for gracefully aborting SSE streams \ 🐛 Fix: address pydantic `union_tag_invalid` errors on Python 3.11 and later ### Go 🐛 Fix: ensure response body is closed after reading \ 🐛 Fix: ensure OAuth2 client credentials hook logic is valid for multiple security schemes ### Java 🐛 Fix: support union of arrays (account for erasure) ### PHP 🐝 Feat: PHP Complex Number support (bigint and decimal) \ 🐝 Feat: const and default support added # automated-code-samples-overlay-management-in-speakeasy-studio Source: https://speakeasy.com/blog/automated-code-samples-overlay-management-in-speakeasy-studio import { ReactPlayer } from "@/lib/mdx/components"; Speakeasy is introducing two powerful updates to enhance your API workflows: ✨ **Automated Code Sample URLs**
We now provide you with a single, stable URL that automatically displays up-to-date SDK code examples. Simply paste this URL into your documentation platform once, and we'll ensure your code samples stay synchronized with your latest API changes. 🔄 **Overlay Management in Speakeasy Studio**
You can now edit your OpenAPI specifications directly within Speakeasy Studio, with every change automatically preserved as an overlay. This means you can freely modify your specs in a familiar environment while keeping your source files unchanged. ## Automated Code Sample URLs Automated Code Sample URLs give you a stable, auto-updating link for SDK code samples. All you have to do is drop that link into your docs platform, and your code examples will stay in sync with your API. Behind the scenes, we dynamically generate SDK-specific code snippets using the x-codeSamples extension in your OpenAPI spec, ensuring your documentation always shows the latest, most accurate examples for your developers. #### What's New? - **Automated SDK Code Samples:** Generate and apply SDK usage examples to your OpenAPI specs effortlessly. - **Seamless Integration:** Supported by popular documentation platforms, including Scalar, Bump.sh, Mintlify, Redocly and ReadMe. - **Always Up-to-Date:** Automatically syncs with your latest SDK updates, ensuring accurate and consistent code snippets. #### How It Works Speakeasy tracks your base OpenAPI document and overlays containing SDK code samples. When you generate an SDK via GitHub Actions and merge the changes to the main branch, Speakeasy automatically creates a Combined Spec that includes all OpenAPI operations along with the generated `x-codeSamples` extensions. These overlays ensure your API documentation includes accurate, language-specific examples tailored to each operation ID. 📖 [Learn more about Automated Code Sample URLs](/docs/automated-code-sample-urls) --- ## Overlay Management in Speakeasy Studio We've made managing OpenAPI specs in Speakeasy Studio simpler and more intuitive. Now you can edit your API specs directly in the Studio, and your changes are automatically saved as overlays—no manual overlay creation needed. This means you can modify your specs without affecting the source, all while staying in the Studio. Here's a quick look at how it works:
### Why This Matters - **Edit without worry:** Make changes directly to your specs and they're automatically saved as overlays - **Stay focused:** Manage all your API modifications in one place - **Learn overlays:** Get greater visibility into overlay functionality while making edits. - **Automatic SDK regeneration:** SDKs are regenerated with each save, ensuring your changes are always reflected. 📖 [Learn More about Overlays](/docs/prep-openapi/overlays/create-overlays) --- ## 🐝 New Features and Bug Fixes 🐛 ### Platform - 🐝 Feat: Introduced Automated Code Sample URLs for simplified SDK example management. - 🐝 Feat: Added overlay management to the Speakeasy Studio. - 🐛 Fix: Resolved issues with outdated examples, missing imports, and incorrect status code handling in test and example generation. - 🐛 Fix: Fixed generation of `example.file` during usage snippet generation. ### Go - 🐛 Fix: Improved mockserver robustness and union usage templating. ### TypeScript - 🐛 Fix: Corrected parameter and global matching for `x-speakeasy-globals`. - 🐛 Fix: Ensured dual-module npm packages include correct metadata. ### Python - 🐛 Fix: Unpinned Python dependencies to prevent conflicts with newer versions. - 🐛 Fix: Added finalizers to close HTTP clients and prevent memory leaks. ### Terraform - 🐝 Feat: Added support for mapping operations to data sources or resources. - 🐝 Feat: Initial support for `x-speakeasy-soft-delete-property`. ### PHP - 🐝 Feat: Enabled unions of arrays and circular unions. - 🐛 Fix: Fixed PHP pagination imports and improved consistency in templating. # ----------------------------------------------------------------------------- Source: https://speakeasy.com/blog/build-a-mcp-server-tutorial This post shows you how to developer an MCP server by hand for education purposes. For anyone who wants to build a production MCP server, Speakeasy can [automatically generate one from your OpenAPI spec](/docs/standalone-mcp/build-server). import { Callout } from "@/mdx/components"; Anthropic recently released the [Model Context Protocol (MCP)](https://modelcontextprotocol.io/introduction), an open protocol and specification for how large language models (LLMs) should interact with external applications. This open protocol and specification is paving the way for a new generation of intelligent tools — tools that can, for example, review GitHub repositories and contributions, provide concise summaries of Slack conversations, or even generate overviews and responses directly within the Claude desktop app. In this article, we'll develop an MCP server that interfaces with the Discord API, enabling it to send, read, and add reactions to specific messages. ## The Model Context Protocol When an application uses an LLM, it often has to give the model context about other applications, the real world, or data on the user's computer. Without direct access to any data outside its training data and the user's inputs, an LLM depends on either the client (such as Anthropic's Claude desktop app) or the user to pass in enough context. What Anthropic offers with MCP is a standardized way for clients to expose arbitrary context to LLMs. Technically, the MCP server sends structured data and prompts to the MCP client, while the protocol layer enforces key behaviors like message framing, request-response matching, and maintaining stateful communication. It operates in a client-server architecture where: - The host is the LLM application (for example, Claude Desktop) that starts the connection. - The client handles communication on behalf of the host, establishing 1:1 connections with servers. - The server provides structured context, tools, and prompts to the client for processing. ## How MCP is used To make it practical, imagine you are using the Claude desktop application to summarize long emails from your manager. You might open each email in Gmail, copy the message text, and then paste it into the Claude app with a request to summarize the message. This is okay for one or two emails, but what if you're trying to summarize twenty messages? To add to this, imagine some of the emails refer to calendar events by date only, without any other details. You'd need to open Google Calendar, find the events, then copy and paste their descriptions. Instead, you could enable the [`mcp-gsuite`](https://github.com/MarkusPfundstein/mcp-gsuite) MCP server and give Claude access to selected tools. This server allows Claude to query your Gmail messages, search your Google Calendar events, and combine all this data to summarize your emails. All the while, Claude keeps you in the loop, requiring authorization to act on your behalf. MCP servers may also expose tools that allow an LLM to act on the world. In our example, you could enable Claude not only to read emails but also to draft or send responses on your behalf. This seems immensely useful; we definitely want to explore it in more depth. ## What we'll build: Allow Claude to chat with Discord We've learned that the best way to learn something new is to get our hands dirty, so here's what we'll do: - Build an MCP server that lets the Claude desktop app interact with a Discord bot. - Create tools that enable Claude to send messages, read messages, and add reactions through the bot, leveraging Discord's core functionalities. You can [find the finished server on GitHub](https://github.com/speakeasy-api/discord-mcp-server-example) or keep reading to see how we built it. ## Prerequisites for building an MCP server If you're following along with the steps we take, you'll need the following installed: - Python 3.11+ - [uv](https://github.com/astral-sh/uv) (the package manager) - [Claude for Desktop](https://claude.ai/download) ## Claude configuration We'll use the Claude app to test our server. We used the macOS version, but there are also versions for Windows. Open the Claude desktop app and navigate to **Settings -> Developer -> Edit Config**. This will open the location of the `claude_desktop_config.json`. ![Claude.app showing the Developer Mode setting](/assets/blog/build-a-mcp-server-tutorial/claude-developer-mode.png) ## Discord configuration In Discord, activate "Developer Mode" by navigating to **Settings -> Advanced -> Developer Mode**. ![Discord Developer Mode](/assets/blog/build-a-mcp-server-tutorial/discord-developer-mode.png) To interact with the Discord API, you'll need a secret key. Get yours on the [Discord developer portal](https://discord.com/developers/applications). Create a Discord application by navigating to **Applications -> New Application**. ![Discord developer portal showing the application creation page](/assets/blog/build-a-mcp-server-tutorial/create-discord-app.png) Give the application a name, accept the terms of service, and click **Create** to create the application. Navigate to the **Bot** tab and click **Reset Token** to generate a new token. ![Discord developer portal showing the bot token page](/assets/blog/build-a-mcp-server-tutorial/generate-discord-secret.png) Copy and make a note of the token, as you will need to use it later. Next, to make sure you can install the bot easily in a Discord server, you need to configure [gateway intents](https://discord.com/developers/docs/events/gateway#gateway-intents), such as the presence intent, server members intent, and message content intent. These will allow your bot to join a server, receive messages, and send messages smoothly. On the **Bot** tab of the Discord developer portal, scroll to the **Privileged Gateway Intents** section and enable the following intents: - Presence Intent - Server Members Intent - Message Content Intent ![Discord developer portal showing the bot intents page](/assets/blog/build-a-mcp-server-tutorial/discord-bot-intents.png) Then, navigate to the **OAuth2** tab and enable the following scopes: - Under **Scopes**, select `bot` - Under **Bot Permissions**, select `Administrator` - Under **Integration type**, leave the default `Guild Install` value selected. ![Discord developer portal bot oauth permissions](/assets/blog/build-a-mcp-server-tutorial/discord-ouath.png) Now copy the **Generated URL** at the end of the page and paste it in a channel in your Discord server to send a message. On the sent message, click the link you pasted and you will be prompted to authorize the bot. Click **Continue**. ![Discord developer portal showing the bot authorization page](/assets/blog/build-a-mcp-server-tutorial/adding-bot-to-channel.png) On the following screen, click **Authorize**. ![Discord developer portal showing the bot authorization page](/assets/blog/build-a-mcp-server-tutorial/discord-bot-authorise.png) Finally, make sure you copy and save the channel IDs of the channel you want to interact with and the server. Right-click on a channel and select **Copy Channel ID**. Right-click on the server and select **Copy Server ID**. ## MCP server project configuration ### Step 1: Create a new MCP server project We'll use uvx to create a new Python MCP server project using [MCP Create Server](https://github.com/modelcontextprotocol/create-python-server). When we built this project, the latest version was v1.0.5. Begin by running the following command in the terminal from the directory where you want to create the project: ```bash uvx create-mcp-server ``` You'll be presented with project-creation options: - For "Project name", use `mcp-discord-chat`. - For "Project description", use `MCP server for interacting with Discord`. - You can leave the default project version and project creation location. - When prompted to install the server for the Claude desktop app, choose `Y` for yes. ```text ? Would you like to install this server for Claude.app? Yes ✔ MCP server created successfully! ✓ Successfully added MCP server to Claude.app configuration ✅ Created project mcp-discord-chat in mcp-discord-chat ℹ️ To install dependencies run: cd mcp-discord-chat uv sync --dev --all-extras ``` To see how this command installed the MCP server, you can take a look at the Claude app configuration file: ```bash cat ~/Library/Application\ Support/Claude/claude_desktop_config.json ``` The file contains a list of servers that Claude can use. You should see the newly created server: ```json filename="~/Library/Application Support/Claude/claude_desktop_config.json" { "mcpServers": { "mcp-discord-chat": { "command": "uv", "args": [ "--directory", "path-to-project/mcp-discord-chat", "run", "mcp-discord-chat" ] } } } ``` Here, you can see that the Claude desktop app runs the server using the `uv` command with the path to the server's directory. The new `mcp-discord-chat` directory contains the following files: ```text . ├── README.md ├── pyproject.toml └── src/ └── mcp_discord_chat/ ├── __init__.py └── server.py ``` ### Step 2: Run the server in the Claude desktop app Let's run the server in the Claude desktop app. Open Claude and click the **🔨 hammer icon** button to open the **Available MCP Tools** dialog: ![Claude.app showing the Available MCP Tools dialog with Discord MCP Server highlighted](/assets/blog/build-a-mcp-server-tutorial/discord-mcp-dialog-init.png) In the dialog, you can see the Discord MCP Server makes a tool called **create-note** available to Claude. Let's try it out. Close the dialog. Type a message that implies you want to create a note and press `enter`. We used the following message: ``` Create a note to remind me to call Grandma when I get home. ``` Claude requests permission to use a tool from the Discord MCP Server. Click **Allow for This Chat**. ![Claude.app showing the permission dialog for the Discord MCP Server](/assets/blog/build-a-mcp-server-tutorial/claude-mcp-dialog-permission.png) Claude shows three messages. ![Claude.app showing the result of the Discord MCP Server tool create_note with the text "I've created a note to remind you to call Grandma when you get home. Is there anything else you'd like me to add to the note?"](/assets/blog/build-a-mcp-server-tutorial/claude-mcp-chat-result.png) The first message appears to be the arguments Claude sent to `add-note`. ```javascript { `name`: `Call Grandma`, `content`: `Remember to call Grandma when you get home.` } ``` The next message looks like a response from the server: ``` Added note 'Call Grandma' with content: Remember to call Grandma when you get home ``` The final message is a response from Claude: > I've added a note to remind you to call Grandma when you get home. Is there anything else you'd like me to help you with? To access a created note from the chat window, open the **Share context with Claude** dialog by clicking the 🔌 **plug connection icon**. If the 🔌 **plug connection icon** isn't visible, you can reveal it by hovering over the 📎 **paperclip icon** to the right of the textbox. ![Claude.app showing the Share context with Claude dialog with the summarize_notes integration selected](/assets/blog/build-a-mcp-server-tutorial/claude-mcp-dialog-summarize.png) In the dialog, click **Choose an integration** and select **summarize_notes**. A new **Fill Prompts Arguments** dialog appears where you can select the style of reply. We'll enter `Be concise` but feel free to change it to your liking. ![Claude asking for the style of reply](/assets/blog/build-a-mcp-server-tutorial/style-prompt.png) Click **Submit** and a `txt` file is attached to the chat as context. The file includes the note we just created, and a prompt asking Claude to summarize the notes. ```txt Here are the current notes to summarize: - Call Grandma: Remember to call Grandma when you get home. ``` Click **Send** to send a blank message with the attached `txt` file to Claude. Claude responds to the prompt and delivers a summary of the notes. ```txt Based on the notes, you have one reminder: - Call Grandma when you get home ``` ### Step 3: Install the Discord SDK and other Python dependencies We want to write the code for the Discord bot, so we need to introduce some changes to the server. We'll use discord.py from PyPI to create our Discord bot. Add the package as a dependency to the `pyproject.toml` file: ```toml filename="pyproject.toml" dependencies = [ "discord.py>=2.3.0", "mcp>=1.2.1", "audioop-lts; python_version >= '3.13'" ] ``` Now run the following command to install the dependencies: ```bash uv sync --dev --all-extras ``` ### Step 4: Add the Discord configuration and imports In the `src/mcp_discord_chat/server.py` file, replace all the code with the following: ```python filename="src/mcp_discord_chat/server.py" import os import asyncio import logging from datetime import datetime from typing import Any, List from functools import wraps import discord from discord.ext import commands from mcp.server import Server from mcp.types import Tool, TextContent from mcp.server.stdio import stdio_server logging.basicConfig(level=logging.INFO) logger = logging.getLogger("discord-mcp-server") DISCORD_TOKEN = os.getenv("DISCORD_TOKEN") if not DISCORD_TOKEN: raise ValueError("DISCORD_TOKEN environment variable is required") intents = discord.Intents.default() intents.message_content = True intents.members = True bot = commands.Bot(command_prefix="!", intents=intents) # MCP Server Initialization # ----------------------------------------------------------------------------- # Create an MCP server instance. The MCP (Model Context Protocol) server will # allow external calls to registered tools (commands) in this application. app = Server("discord-server") # Global variable to store the Discord client instance once the bot is ready. discord_client = None ``` This code initializes a Discord bot using discord.py and integrates custom MCP server functionality. It performs environment checks for required variables, configures logging, and sets up bot intents and command prefixes. Key steps include: - Verifying that the `DISCORD_TOKEN` environment variable is set. - Creating a bot instance with appropriate permissions and a command prefix. - Initializing an MCP server instance and preparing a placeholder for the Discord client. ### Step 5: Add helper functions The next step is to add helper functions for data formatting and repetitive tasks. ```python filename="src/mcp_discord_chat/server.py" def format_reactions(reactions: List[dict]) -> str: """ Format a list of reaction dictionaries into a human-readable string. Each reaction is shown as: emoji(count). If no reactions are present, returns "No reactions". """ if not reactions: return "No reactions" return ", ".join(f"{r['emoji']}({r['count']})" for r in reactions) def require_discord_client(func): """ Decorator to ensure the Discord client is ready before executing a tool. Raises a RuntimeError if the client is not yet available. """ @wraps(func) async def wrapper(*args, **kwargs): if not discord_client: raise RuntimeError("Discord client not ready") return await func(*args, **kwargs) return wrapper ``` ### Step 6: Add an event handler Now add the Discord event handler after a successful login: ```python filename="src/mcp_discord_chat/server.py" @bot.event async def on_ready(): """ Event handler called when the Discord bot successfully logs in. Sets the global discord_client variable and logs the bot's username. """ global discord_client discord_client = bot logger.info(f"Logged in as {bot.user.name}") ``` ### Step 7: Add the tools Tools in the MCP server enable servers to expose executable functionality to clients, allowing Claude to interact with external systems according to instructions provided in the server code. We will add three tools to: - Read messages from a channel. - Send messages to a channel. - Add a reaction to a message in a channel. ```python filename="src/mcp_discord_chat/server.py" @app.list_tools() async def list_tools() -> List[Tool]: return [ Tool( name="add_reaction", description="Add a reaction to a message", inputSchema={ "type": "object", "properties": { "channel_id": { "type": "string", "description": "ID of the channel containing the message", }, "message_id": { "type": "string", "description": "ID of the message to react to", }, "emoji": { "type": "string", "description": "Emoji to react with (Unicode or custom emoji ID)", }, }, "required": ["channel_id", "message_id", "emoji"], }, ), Tool( name="send_message", description="Send a message to a specific channel", inputSchema={ "type": "object", "properties": { "channel_id": { "type": "string", "description": "Discord channel ID where the message will be sent", }, "content": { "type": "string", "description": "Content of the message to send", }, }, "required": ["channel_id", "content"], }, ), Tool( name="read_messages", description="Read recent messages from a channel", inputSchema={ "type": "object", "properties": { "channel_id": { "type": "string", "description": "Discord channel ID from which to fetch messages", }, "limit": { "type": "number", "description": "Number of messages to fetch (max 100)", "minimum": 1, "maximum": 100, }, }, "required": ["channel_id"], }, ), ] ``` Each tool has a name that should be unique, a description, and an input schema. Now that the tools are defined, we can write the dispatch function for tool calls. ```python filename="src/mcp_discord_chat/server.py" @app.call_tool() @require_discord_client async def call_tool(name: str, arguments: Any) -> List[TextContent]: if name == "send_message": # Retrieve the channel and send the message with the provided content. channel = await discord_client.fetch_channel(int(arguments["channel_id"])) message = await channel.send(arguments["content"]) return [ TextContent( type="text", text=f"Message sent successfully. Message ID: {message.id}" ) ] elif name == "read_messages": # Retrieve the channel and fetch a limited number of recent messages. channel = await discord_client.fetch_channel(int(arguments["channel_id"])) limit = min(int(arguments.get("limit", 10)), 100) messages = [] async for message in channel.history(limit=limit): reaction_data = [] # Iterate through reactions and collect emoji data. for reaction in message.reactions: emoji_str = ( str(reaction.emoji.name) if hasattr(reaction.emoji, "name") and reaction.emoji.name else ( str(reaction.emoji.id) if hasattr(reaction.emoji, "id") else str(reaction.emoji) ) ) reaction_info = {"emoji": emoji_str, "count": reaction.count} logger.debug(f"Found reaction: {emoji_str}") reaction_data.append(reaction_info) messages.append( { "id": str(message.id), "author": str(message.author), "content": message.content, "timestamp": message.created_at.isoformat(), "reactions": reaction_data, } ) # Format the messages for output. formatted_messages = "\n".join( f"{m['author']} ({m['timestamp']}): {m['content']}\nReactions: {format_reactions(m['reactions'])}" for m in messages ) return [ TextContent( type="text", text=f"Retrieved {len(messages)} messages:\n\n{formatted_messages}" ) ] elif name == "add_reaction": # Retrieve the channel and message, then add the specified reaction. channel = await discord_client.fetch_channel(int(arguments["channel_id"])) message = await channel.fetch_message(int(arguments["message_id"])) await message.add_reaction(arguments["emoji"]) return [ TextContent( type="text", text=f"Added reaction '{arguments['emoji']}' to message {message.id}" ) ] # If the tool name is not recognized, raise an error. raise ValueError(f"Unknown tool: {name}") ``` When the dispatch function is called, it compares the `name` parameter to predefined tool names to ensure the correct function is executed, and then carries out the corresponding task. For example, when the dispatch function is called with `read_messages`, it retrieves a limited number of recent messages from the specified channel, processes each message to extract details such as author, content, timestamp, and reactions, and then formats these details into a readable output. Make sure the last lines of the file look like this: ```python filename="src/mcp_discord_chat/server.py" async def main(): asyncio.create_task(bot.start(DISCORD_TOKEN)) # Open a connection using the stdio server transport and run the MCP server. async with stdio_server() as (read_stream, write_stream): await app.run(read_stream, write_stream, app.create_initialization_options()) # ----------------------------------------------------------------------------- # Application Entry Point # ----------------------------------------------------------------------------- if __name__ == "__main__": asyncio.run(main()) ``` Finally, modify the `claude_desktop_config.json` file to include the `DISCORD_TOKEN` environment variable, to ensure it's injected when the server is running. ```json filename="~/Library/Application Support/Claude/claude_desktop_config.json" { "mcpServers": { "discord": { "command": "uv", "args": [ "--directory", "path-to-project/mcp-discord-chat", "run", "mcp-discord-chat" ], "env": { "DISCORD_TOKEN": "YOUR_DISCORD_TOKEN" } } } } ``` ## Test the MCP Discord Chat server Now we can test the MCP server in the Claude desktop app. First, make sure that you have the tools listed in the application. ![MCP Discord Tools](/assets/blog/build-a-mcp-server-tutorial/claude-discord-mcp-tools.png) Now get the channel and server IDs from the Discord application and send the following message: ```txt Hi! Here are the channel ID and the server ID for the Discord app. What is the previous message in this channel? ``` You'll get a response similar to this: ![MCP Discord Read Message](/assets/blog/build-a-mcp-server-tutorial/claude-discord-mcp-tools-read-messages.png) You can tell Claude to add a reaction to the message: ```txt Add a reaction to the last message in this channel. ``` Send a message to prompt Claude to post to the channel: ```txt Say Hi in the channel. ``` ## Tips for working with MCP servers We encountered a few stumbling blocks while building our MCP server. Here are some tips to help you avoid the same issues. ### 1. Environment variables: Pass them to your server By default, MCP servers have limited access to environment variables. Either pass the environment variables as part of the server configuration for your MCP client or use a `.env` file in the root of your server project. ```json filename="~/Library/Application Support/Claude/claude_desktop_config.json" { "mcpServers": { "discord": { "command": "uv", "args": [ "--directory", "path-to-project/mcp-discord-chat", "run", "mcp-discord-chat" ], "env": { "DISCORD_TOKEN": "YOUR_DISCORD_TOKEN" } } } } ``` ### 2. Debugging: Beware of `STDOUT` MCP servers communicate with clients through `stdio`. Be mindful of what you log to `STDOUT` and `STDERR`, as this output is sent to the client. We ran into issues debugging our server because logging errors to the console caused the messages to fail. Instead, use `console.error()` to log errors in a JavaScript environment or the logging tool provided by the MCP server SDK. ### 3. MCP clients: Our options are currently limited in number and capabilities We tried Claude as our first MCP client, then tried a couple of others. - [Claude](https://claude.ai/download) was the most feature-rich, but struggled with complex interactions. - [Zed](https://zed.dev/) was the most basic, and since it doesn't support MCP tools yet (we only found out after building our server), we couldn't test our server with it. - [Cline](https://github.com/cline/cline) was helpful, as it supports MCP tools, but it hallucinated a server name for us, which was a bit confusing. It self-corrected after the first try, though. ## Closing thoughts This was a fun project for becoming familiar with MCP. We only used one of the many message types supported by the protocol. New MCP servers are popping up every day, and we're excited to see what people build. We hope it takes off and becomes, as we've heard commenters wish, "the USB-C of AI integrations." ## Resources - [Model Context Protocol](https://modelcontextprotocol.io/) - The official MCP website - [discord.py](https://discordpy.readthedocs.io/en/stable/) - The discord.py documentation - [Anthropic](https://anthropic.com/) - The creators of MCP and Claude # build-custom-workflows-sneak-peak Source: https://speakeasy.com/blog/build-custom-workflows-sneak-peak Nothing is more frustrating than a platform standing in the way of you solving your problem. That is why the best platforms provide users extensibility. When a use case is not served by the core platform, users can extend the platform with customized functionality to address their use case. The Speakeasy API platform is no different. We've packed a lot of functionality into it, but we can't do everything. Which is why we're excited to give our users the ability to build plugins that add custom functionality to the platform. ## New Features **Speakeasy** **Plugins [Alpha]:** Users can now define custom javascript functions (powered by WebAssembly!) to run on the API request & response data that flows into the Speakeasy platform. The possible applications are endless, but some common uses would be like custom field validation script, defining a derived field from the raw data, or tracing a user onboarding onto your API. The newly defined fields are then added to the existing set of api requests for you to view. We plan for plugins to be hosted on Github with an extendable base template to get you started. Eventually, we want to explore exposing these to directly to your end users. We can't wait to see what you all come up with! See the new feature in action below: ## Small Improvements **[Bug fix] CLI Auth for Safari & Brave** - After reports of CLI authentication running into CORS issues on Safari & Brave browsers, we've switched our authentication method to one compatible with all browsers. **CLI Safe Cleanup** - Our CLI is getting smarter! It now tracks which files it generates when building SDKs and will only purge previously created files before new output is saved. # Building a SaaS API? Don't Forget Your Terraform Provider Source: https://speakeasy.com/blog/build-terraform-providers import { Table } from "@/mdx/components"; Replacing custom integrations with Terraform cuts down on errors, simplifies infrastructure management, makes infrastructure changes easier to version, and saves developers hours of repeatedly clicking around hundreds of different dashboards. Most users are familiar with Terraform DevOps functions: Launching web servers, managing databases, and updating DNS records on AWS, GCP, and Azure. But Terraform's adoption is growing far beyond infrastructure and it's time we free this sleeping giant from its reputation as a cost center. Instead, we see Terraform as a strategic ally and driver of revenue for most SaaS platforms through the offering of a provider. Terraform with its provider plugin ecosystem can serve as a robust interface between your SaaS API and your users, allowing for a more integrated, efficient, and scalable developer experience. It's not just about deploying servers and managing databases anymore; it's about creating a unified and streamlined workflow for your users, reducing complexities, and unlocking new use cases. Terraform already helps your users solve their infrastructure problems: Speakeasy makes it straightforward to create API surface areas that provide exceptional developer experience to your users. That includes SDKs in 7+ popular languages, but it also means [Terraform providers](/docs/create-terraform). We help you meet your users where they already are - the Terraform registry. But first, let's start with some background. ## What is Terraform? Terraform is an open-source infrastructure-as-code (IaC) tool developed by HashiCorp. IaC is essential in modern DevOps practices as it allows for consistent and repeatable deployments, minimizing the risks and tedium of manual configurations. By describing their desired infrastructure in a declarative language called HCL (HashiCorp Configuration Language), teams can version, share, and apply infrastructure definitions using Terraform. The most basic building blocks of Terraform configurations are Terraform providers. ## What Is a Terraform Provider? A Terraform provider is a plugin that allows Terraform to manage a given category of resources. Providers usually correspond to specific platforms or services, such as AWS, Azure, GCP, or GitHub. Each provider defines and manages a set of resource types—for example, an AWS provider might handle resources like an AWS EC2 instance or an S3 bucket. When you're writing your Terraform configuration files, you define what resources you want and which provider should manage those resources. The provider is then responsible for understanding API interactions with the given service and exposing resources for use in your Terraform scripts. In practical terms, this means that providers translate the HCL code that users write into API calls to create, read, update, delete, and otherwise manage resources on these platforms. By using providers, Terraform users can manage a wide variety of service types. The most widely used Terraform plugin registry is the [HashiCorp Terraform registry](https://registry.terraform.io/). Launched in 2017, the registry [has now surpassed 3,000 published providers](https://www.hashicorp.com/blog/hashicorp-terraform-ecosystem-passes-3-000-providers-with-over-250-partners). ## Terraform Beyond Infrastructure While Terraform's primary function remains infrastructure management, its use cases extend beyond the traditional scope. In addition to managing servers, databases, and networks, Terraform can be used to manage higher-level services and applications, including SaaS products. Let's look at a few examples of Terraform providers for SaaS platforms: ### LaunchDarkly Terraform Provider [LaunchDarkly](https://launchdarkly.com/) is a continuous delivery platform that enables developers to manage feature flags and control which users have access to new features. For each new feature your team builds, you add a feature flag and gradually release the feature to your users based on certain criteria. Now imagine a situation where you want to test a feature flag in development, staging, QA, and then finally release it in production. Your team would need to log in to the LaunchDarkly dashboard each time to manage these flags across the different environments, which can be a time-consuming process. The [LaunchDarkly Terraform provider](https://registry.terraform.io/providers/launchdarkly/launchdarkly/latest/docs) allows developers to automate the process of creating, updating, and deleting feature flags across different environments, reducing manual effort, minimizing human error, and increasing efficiency in their workflows. It's a clear win for developer productivity and reliability of the deployment process. ### Checkly Terraform Provider [Checkly](https://www.checklyhq.com/) enables developers to monitor their websites and APIs, offering active monitoring, E2E testing, and performance metrics. It provides essential insights into uptime, response time, and the correctness of your web services. Imagine a situation where your organization has multiple services, each with different endpoints. Managing these services and ensuring they all maintain a high level of performance can be a daunting task. You may also require your developers to use Checkly headless browser testing or screenshot features in development, staging, and other environments. You'd need to log in to the Checkly dashboard each time to set up or adjust monitoring configurations, a process that could become tedious and time-consuming, especially in large-scale environments. The [Checkly Terraform provider](https://registry.terraform.io/providers/checkly/checkly/latest/docs) simplifies this process by allowing developers to automate the configuration and management of checks and alerts. Instead of manually configuring each check via the Checkly dashboard, developers can define them directly in their Terraform configurations. This means checks can be versioned, shared, and managed just like any other piece of infrastructure. ### Other SaaS Terraform Providers - [PagerDuty](https://registry.terraform.io/providers/PagerDuty/pagerduty/latest/docs): Developers benefit from the PagerDuty Terraform provider by being able to automate the set up and management of incident response procedures, providing a consistent, repeatable, and efficient way to manage complex operational environments. - [Salesforce](https://registry.terraform.io/providers/hashicorp/salesforce/latest/docs): The Salesforce Terraform provider allows administrators to programmatically manage Salesforce users. This provider is released by Hashicorp and may have been released to scratch their own itch while administering Salesforce, but it is a clear indication that Terraform has at least some usage outside of development teams. - [Fivetran](https://registry.terraform.io/providers/fivetran/fivetran/latest/docs): With the Fivetran Terraform provider, users can automate the set up and management of data connectors, allowing for easy and efficient integration of various data sources with their data warehouse in a version-controlled manner. - [GitHub](https://registry.terraform.io/providers/integrations/github/latest/docs): GitHub users benefit from its Terraform provider by being able to automate repository management, actions, teams, and more, streamlining workflows and improving efficiency across their development teams. Admittedly, the lines between SaaS and infrastructure are blurred for some of these, but the examples above are only a fraction of what's available on the registry. ## Why Users Choose Terraform Over API Integration All the SaaS platforms in our examples above have full-featured and well-documented APIs with accompanying SDKs. Chances are, if you're not using Terraform in your organization yet, you may come across some utility code in your repositories performing regular calls to one of these ubiquitous platforms' APIs. We touched on a couple of examples where Terraform enables teams to configure services for use with different environments in the development lifecycle. This unlocks new use cases, and may even increase many users' spending after automation negates the labor cost of configuring ephemeral environments. Furthermore, it seems the availability of good Terraform providers is already starting to factor into SaaS purchasing decisions. Organizations that are audited as part of certification often have strict disaster recovery requirements. Terraform providers with full API coverage could enable these organizations to recover their SaaS configurations without any manual work. This benefit also applies when organizations are required to keep track of configuration updates. If a SaaS platform does not have strict auditing built in, a client could use Terraform with version control, thereby creating an audit trail. ## Which SaaS Products Are Good Candidates for Terraform? With the benefits we mentioned in mind, we can try to identify traits that would make a SaaS platform a suitable candidate for Terraform integration. - **Configurable resources:** SaaS platforms that offer configurable resources like users, roles, policies, projects, or servers are good candidates. If a resource's configuration can be described in code, it can be managed by Terraform. - **Multiple environments:** Platforms that need to maintain consistent configurations across multiple environments (like dev, test, staging, and production) are well-suited for Terraform, as it simplifies the creation and management of these environments. - **Frequent changes:** If your SaaS product requires frequent changes to its configuration, it may benefit from a Terraform provider. With its plan/apply cycle, Terraform allows users to preview changes before applying them, reducing the risk of unintentional modifications. - **Scaling needs:** SaaS platforms that need to manage resources at scale can also benefit from Terraform. It allows for managing many resources consistently and efficiently, reducing the risk of manual errors and inconsistencies. - **Automatable tasks:** If your platform's tasks can be automated via API calls, then a Terraform provider would be a good fit. Terraform excels in automating infrastructure management tasks and reduces the need for manual intervention. - **Security and compliance needs:** For SaaS platforms that need to enforce certain security configurations or compliance standards, Terraform can ensure these requirements are consistently met across all resources. - **Integrations with other cloud services:** If your SaaS platform frequently integrates with other cloud services, having a Terraform provider can make these integrations easier and more efficient by leveraging Terraform's extensive provider ecosystem. Even if a SaaS platform doesn't check all these boxes, there might still be significant value in offering a Terraform provider, especially as Terraform's adoption is growing rapidly. ## What About Internal Tools and Interfaces? Terraform providers are especially useful for building internal tools, where they provide your internal developers an AWS-like experience while managing internal resources. By offering a Terraform provider to internal users, organizations can provide a consistent, standardized interface for managing internal resources. This standardization reduces the learning curve and complexity for developers, as they can use the same Terraform commands and concepts they're already familiar with from managing public cloud resources. The benefits we mentioned earlier also apply to internal tools: Better collaboration, version control, reduced errors, and less time configuring services using manual interfaces. ## Why SaaS Companies Don't Publish Terraform Providers Clearly, publishing a Terraform provider could benefit most SaaS providers, so why don't we see more companies maintaining Terraform providers for their APIs? We won't mince words here: Publishing a Terraform provider is difficult. Telling SaaS development teams to _“just publish a Terraform provider”_ would be misguided at best. ![If only it was as easy as drawing an owl](/assets/blog/build-terraform-providers/owl.jpg) Developing and maintaining a Terraform provider requires a significant investment in terms of time, resources, and expertise. You need an in-depth understanding of the SaaS platform's API, the Terraform ecosystem, and the Go programming language. Add to this the fact that many APIs change significantly over time. If an API changes frequently, it can require a significant effort to keep its Terraform provider up to date. Even if creating and maintaining a provider is within a SaaS company's abilities, there might be hesitance to take on an additional support commitment. We understand that it could feel like you're adding another layer to an already complex problem and users will expect some manual help. We argue that the simplicity of HCL lends itself to much easier support engineering, as declarative configuration is simpler to lint automatically, read, debug, and rewrite. Terraform is well suited for self-help users, as the documentation for Terraform providers is standardized and hosted by the registry. Nevertheless, some platforms such as LaunchDarkly choose to support Terraform integration only for users on pro or enterprise pricing tiers—presumably to offset anticipated support cost. ## Speakeasy Generates and Maintains Terraform Providers With Speakeasy, you can generate a Terraform provider based on your OpenAPI spec. This means you don't need to be a Terraform expert or write any custom Go code. Speakeasy also makes sure your provider stays updated with your API by pushing a new branch to your provider's repository when your API spec changes. To generate a Terraform provider, you map OpenAPI objects and operations to Terraform entities and actions by annotating your OpenAPI specification. ![Workflow diagram showing Speakeasy's Terraform generation](/assets/blog/build-terraform-providers/tf-workflow.png) To get started, you can follow our [documentation on annotating your spec for the Terraform provider](/docs/terraform/create-terraform#add-annotations) generator. After generating a provider, updating the provider becomes as straightforward as merging a PR from the update branch Speakeasy creates. ## Case Study: Airbyte Terraform Provider On 22 June, [Airbyte launched their Terraform provider](https://airbyte.com/blog/airbytes-official-api-and-terraform-provider-now-in-open-source) for Airbyte cloud users. The Airbyte [Terraform provider](https://registry.terraform.io/providers/airbytehq/airbyte/latest/docs) was generated by Speakeasy, based entirely on Airbyte's OpenAPI specification. This release came after months of collaboration between Airbyte and Speakeasy, and we are delighted to have played a role in Airbyte's continued success. > We looked for the best options in API tooling, so we didn't have to build everything ourselves. We focus on what we do best: ensuring data is accessible everywhere it has value. For our API needs; we have Speakeasy. > > -- Riley Brook, Product @ Airbyte Under the hood, the Airbyte Terraform provider uses a Go SDK generated by Speakeasy. Since the [generated provider is open source](https://github.com/airbytehq/terraform-provider-airbyte), we can take a look at what's in the repository. We ran [Sloc Cloc and Code (scc)](https://github.com/boyter/scc/blob/master/README.md) on the repository and this is what we found: ```bash $ scc terraform-provider-airbyte ```
- Estimated Cost to Develop (organic) $10,705,339 - Estimated Schedule Effort (organic) 33.86 months - Estimated People Required (organic) 28.09 Airbyte connects with more than 250 data sources and destinations, all with unique configuration parameters, which adds to this enormous Terraform provider. Even if the scc estimation is off by a few orders of magnitude, it is clear that the Speakeasy Terraform provider generator saved Airbyte valuable development time and will continue to save time on maintenance in the future. You can read more about [how Airbyte launched their SDKs and Terraform provider](/post/case-study-airbyte) on our blog. ## Summary We believe that Terraform is poised to simplify SaaS configuration at scale and expect to see the continued growth of the Terraform registry. However, navigating the Terraform ecosystem without deep expertise is daunting, and successfully publishing and maintaining a Terraform provider is no small undertaking. Speakeasy is ready to help SaaS platforms expand their reach in this exciting ecosystem. To get started with your Terraform provider, [follow our documentation](/docs/create-terraform). [Join our Slack community](https://go.speakeasy.com/slack) for expert advice on Terraform providers or get in touch to let us know how we can best help your organization. # Building APIs (and SDKs) that never break Source: https://speakeasy.com/blog/building-apis-and-sdks-that-never-break We've all seen websites that suddenly stop working for seemingly no reason. Mobile apps that load infinitely. Smart fridges that stop... well, fridging. In early 2025 I shipped a new screen that highlighted the amazing features available in Monzo's business subscription offering. Except - for some people using iOS - it didn't. Turns out the server-driven view framework didn't support a certain type of image view on a specific range of iOS app versions. This meant the entire feature table was just empty. Shipping changes constantly, with high confidence is a quintessential component of developer velocity and business reputation. Annoyingly, **APIs don't just break when you change the contract. They break when you change _behaviour that somebody depended on_.** If you publish an SDK or an OpenAPI spec (which you definitely should), you've made the problem harder, not easier. Because now you don't just have "the API". You have: - the API behaviour - the OpenAPI spec people generate clients from - the SDK behaviour and its runtime validation - customers' code written against all of the above Those layers drift. Constantly. Simply put: SDK users can get a `200 OK` from the server and still see an SDK error, often due to API evolution or spec drift combined with strict client-side validation. So let's talk about how to build APIs (and SDKs) that _don't_ turn every change into an incident. --- ## Why API versioning matters APIs are contracts. The moment you expose an endpoint to consumers — whether those consumers are third-party developers, mobile apps, or internal services — you've made a promise about how that endpoint behaves. Breaking that promise has consequences. That matters even more when you have clients you don't control: - Mobile apps versions lag behind. Especially if automatic updates are disabled. - Customers have release cycles, change control, procurement, security reviews. - Some integrations are "set and forget" until they fail. When was the last time you reviewed your Slack bot implementations? If you maintain an SDK or a public OpenAPI spec, your "real API" is bigger than your HTTP surface area. The goal isn't to avoid change—that's impossible. The goal is to evolve your API while giving consumers a clear, predictable path forward. So the core problem is **drift**: - API evolves, spec lags - spec changes, SDK lags - SDK changes, customers lag - customers do weird things you never anticipated Which brings us to the fun part: how exactly things break. --- ## Types of breakages I'll use Go for backend examples and TypeScript for client examples, but these concepts are language-agnostic. ### Removing a property This is the classic "we're cleaning up the response shape". ```diff type Payee struct { ID string `json:"id"` Name string `json:"name"` - ViewDetails *ViewDetails `json:"view_details,omitempty"` } - type ViewDetails struct { - URL string `json:"url"` - } ``` #### Client break (TypeScript) ```typescript const payee = await client.payees.get("p_123"); // worked yesterday window.location.href = payee.viewDetails.url; ``` Typical runtime result: ``` TypeError: Cannot read properties of undefined (reading 'url') ``` The field may have already been optional, but if you taught your users to expect it, they will. --- ### Adding a new enum variant This one catches teams off guard because it _feels additive_. GitHub's GraphQL docs make an important distinction: adding an enum value is often a **dangerous** change — it might not break queries, but it can break client runtime behaviour. ```diff { - "status": "created" + "status": "paused" } ``` A lot of SDKs (handwritten or generated) validate enums on deserialisation: ```typescript import * as z from "zod"; const PayeeSchema = z.object({ // ... status: z.union(["created", "active"]), }); const payees = { async get(id: string) { const result = await fetch(`/payees/${id}`); const json = await result.json(); // new "paused" status will cause a failure here return PayeeSchema.parse(json); }, }; ``` Now `"paused"` bricks the whole response. Even if our SDK didn't validate the response, or used a forward-compatible enum strategy, our users could still have perfectly compiling code that breaks at runtime. ```typescript const badgesByPayeeStatus: Record = { created: "badge-neutral", active: "badge-active", }; const payee = await sdk.payees.get(payeeId); const badge = badgesByPayeeStatus[payee.status]; // type is 'string', but could be undefined ``` Versioning on its own may not be enough to fully mitigate this case. If you anticipate your API evolving in this dimension, I recommend starting with a fallback value, such as `"unknown"` and defaulting to that in SDKs or transforming unexpected variants to the fallback variant, based on client version. --- ### Renaming a property This is "let's improve consistency" — and it's unavoidably breaking unless you support both names. ```diff { - "date_of_birth": "1990-01-01" + "dob": "1990-01-01" } ``` ```typescript const dob = new Date(payee.dateOfBirth); // now undefined ``` Or worse: ```typescript const year = payee.dateOfBirth.slice(0, 4); // TypeError: Cannot read properties of undefined (reading 'slice') ``` Who would blindly rename a field like that – you might ask. How about renaming less obvious parts of the response, e.g.: headers or file extensions. How about simply renaming a file extension from `yml` to `yaml`? Nobody depends on those, right? Wrong. Introducing: Hyrum's Law... --- ### Hyrum's Law: unexpected failures Put succinctly, Hyrum's Law is: > With a sufficient number of users of an API, it does not matter what you promise in the contract: all observable behaviours of your system will be depended on by somebody. This is the bit that makes "safe changes" unsafe. #### Innocent change: adding a new property You add `nickname` to a response. You didn't remove anything. What could go wrong? Client-side strict schema validation: ```typescript import { z } from "zod"; const PayeeSchema = z .object({ id: z.string(), name: z.string(), }) .strict(); // rejects unknown keys const payee = PayeeSchema.parse(apiResponse); ``` Now `nickname` causes the parse to throw. Stripe's versioning scheme explicitly lists "adding new properties" and even "changing the order of properties" as _backward-compatible_ changes — but that compatibility assumes clients don't do brittle validation. A technique to combat uncontractual expectations is response scrambling. This is when you intentionally introduce variance to parts of your API you consider unstable or not guaranteed. E.g.: Stop users from building reliance on sort order by shuffling arrays in response. ```go transactions := transactionService.Read(ctx) if req.Sort == "" { // NOTE: order of transactions is not guaranteed, unless req.Sort is specified transactions = sliceutil.Shuffle(transactions) } ``` #### Innocent change: field order changes (tuples / compact formats) Most JSON objects are unordered, but plenty of APIs return compact array-based payloads for performance: ```diff { - "columns": ["id", "name"], + "columns": ["name", "id"], - "rows": [["p_123", "Daniel"]] + "rows": [["Daniel", "p_123"]] } ``` Client code that maps by index: ```typescript const [id, name] = rows[0]; // now swapped ``` Not a crash — just corrupted data. Arguably, this silent failure is worse than a crash, because it likely won't show up in logs or monitoring. #### Innocent change: new field exists, but the SDK strips it This is a subtle one and it's _infuriating_ as a customer. - API ships a new field today - you want to use it today - SDK won't let you, because it strips unknown properties (or validates strictly) - SDK release comes later - your feature is blocked by someone else's release cadence A simplified SDK deserialiser: ```typescript type Payee = { id: string; name: string; }; function decodePayee(raw: any): Payee { // drops everything unknown return { id: raw.id, name: raw.name, }; } ``` So even though the wire response includes `viewDetails`, the SDK won't expose it. My colleague, David wrote an entire article on this class of problem: strict client validation + API evolution = confusing breakages where the server "succeeds" but the SDK still errors. [Read more about it here](https://www.speakeasy.com/blog/typescript-forward-compatibility). --- ## Versioning approaches There's no single correct strategy. There are serious trade-offs to each approach. I recommend reading through the pros and cons and devising your own strategy, based on your needs. Not sure what's best for you product, or need help setting up automations? Reach out! I love chatting about all things API design. ### How Stripe does versioning [Stripe's 2017 write-up](https://stripe.com/blog/api-versioning) is still one of the best "under the hood" explanations I've read. They model responses as API resources and apply versioned transformations when breaking changes happen. Conceptually: - There is a "current" schema - Older versions are produced by applying a set of transformations - A request specifies which version it wants (often defaulting to an account-pinned version) Stripe also exposes version overrides via the `Stripe-Version` header. And more recently, Stripe introduced a release cadence with named major releases (with breaking changes) and monthly backward-compatible releases. #### What it looks like in practice Stripe uses Ruby for most of their backend services. Because all backend examples are in Go and to prove that this pattern is simply reproducible in the programming language of your choice, I re-wrote the examples from Stripe's blog post in Go. ```go type APIVersion string const ( V2024_01_01 APIVersion = "2024-01-01" V2024_06_01 APIVersion = "2024-06-01" ) type PayeeCanonical struct { ID string `json:"id"` Name string `json:"name"` DateOfBirth string `json:"date_of_birth"` // canonical ViewDetails *ViewDetails `json:"view_details"` } type Transformer func(map[string]any) map[string]any func renameField(from, to string) Transformer { return func(m map[string]any) map[string]any { if v, ok := m[from]; ok { m[to] = v delete(m, from) } return m } } func dropField(field string) Transformer { return func(m map[string]any) map[string]any { delete(m, field) return m } } var versionTransforms = map[APIVersion][]Transformer{ V2024_06_01: { // current: no transforms }, V2024_01_01: { // older clients expect dob, not date_of_birth renameField("date_of_birth", "dob"), // older clients do not expect view_details dropField("view_details"), }, } func render(version APIVersion, canonical PayeeCanonical) ([]byte, error) { // marshal canonical -> map so we can transform raw, _ := json.Marshal(canonical) var m map[string]any if err := json.Unmarshal(raw, &m); err != nil { return nil, err } for _, t := range versionTransforms[version] { m = t(m) } return json.Marshal(m) } ``` The point is: **backward compatibility costs ongoing effort**. Brandur explicitly frames versioning as a compromise between DX improvements and the burden of maintaining old versions. **Pros** - It's lightweight. Upgrade friction is minimal. Each version contains an incremental set of changes rather than a massive rewrite. - Versioning is integrated deeply into tooling and documentation. - Old versions are tightly encapsulated. The happy-path (current version) is the default, backwards compatibility is the bolt-on feature. **Cons** - Current-backward model requires engineers to implement all changes twice: forward and back. - Limited to changes that can be expressed in transformations. - Side-effects don't have first class support. #### TypeScript SDK: letting users pin a version In "Stripe-like" ecosystems, you usually see something like: ```typescript const sdk = new Client({ apiVersion: "2024-06-01", }); ``` or version supplied per request / per client instance, often mapped to a header. That's important because it makes version choice **explicit and testable**, instead of implicit magic. --- ### Monzo's versioning Before Speakeasy, I worked for Monzo, UK's most popular neobank. Building APIs for mobile apps means versioning was non-negotiable. Apps are hard to update reliably. iOS in particular can lag for months in the real world: old devices, disabled updates, people on holiday, App Store review delays, the lot. A sizeable percentage of our users had auto-updates disabled. Monzo uses explicit server-side app version checks to shape responses based on client version, because you _cannot_ assume everyone upgraded. A simplified illustration of that approach: ```go // DISCLAIMER: not actual Monzo code - for illustration purposes only type Payee struct { Name string `json:"name"` DateOfBirth time.Time `json:"date_of_birth"` Status PayeeStatus `json:"status"` // ViewDetails is only handled by iOS 13.3.0+ and Android 12.3.5+ ViewDetails *ViewDetails `json:"view_details,omitempty"` } func PayeeHandler(ctx context.Context) (*Payee, error) { payee := Payee{ Name: "Daniel", DateOfBirth: time.Date(1990, 1, 1, 0, 0, 0, 0, time.UTC), Status: PayeeStatusCreated, } // Version check determines feature availability if version.FromCtx(ctx).GreaterOrEqual( cond.Or(version.iOS(13, 3, 0), version.Android(12, 3, 5)), ) { var err error payee.ViewDetails, err = buildExpensiveViewDetailsForNewApps(ctx) if err != nil { return nil, err } } return &payee, nil } ``` The idea is straightforward: you treat "client version" as an input to response rendering. **But there are caveats.** In practice, you hit questions like: 1. **Feature flags and experiments** - Do you let your feature flag platform handle all targeting (including version checks)? - Or do you keep version checks in code and only call the flag system when the client is capable? Feature flag products (Statsig is one example) exist specifically to toggle behaviour without redeploying code. The tradeoff is operational simplicity vs performance/clarity vs "how many moving parts are involved in a rollout". 2. **Version → feature mapping drift** - If your code has scattered `if version >= X` checks, nobody can answer "which versions support which features" without grep. - Eventually you get a matrix of doom: version checks, platform checks, experiments, entitlement checks. 3. **Async contexts** - Code triggered _by an API request_ has client context. - Background jobs often don't. - If your fallback is "last known version", you're depending on a state that can be stale or missing. This strategy works well when you control most clients (mobile, first-party apps). It's harder to scale cleanly to public APIs, where you need explicit contracts and explicit lifecycles. **Pros** - Client-driven: uses a well-known property of the client (app build version). No additional work is required on the client-side (other than forwarding the version). - Versioned by default. Every breaking change will have an `if` statement and a version check associated with it. - Easy to support with test tooling, versioning issues are often caught by unit tests. **Cons** - Not easy to extend to more consumers. New clients, e.g.: `web` won't support the latest version by default, unless explicitly updated. - Relationship between versions and feature support becomes tribal knowledge. There's no way to associate a version with a feature set at a glance. - Makes it non-trivial to re-use logic across request handlers and other logic, e.g.: in stream consumers, the context wouldn't hold an app version. --- ## Other strategies ### Version prefix The classic: - `GET /api/v1/users/123` - `GET /api/v2/users/123` **Pros** - Easy to explain. - Tooling-friendly (OpenAPI, gateways, routing). - Clear separation. **Cons** - It turns versioning into a heavyweight decision: "is this a v2 moment?" - Teams delay necessary changes because they fear "the v2 project". - Or they do the opposite: `/v1212/` and customers lose confidence. Minimal Go routing example: ```go http.HandleFunc("/api/v1/users/", v1UserHandler) http.HandleFunc("/api/v2/users/", v2UserHandler) ``` TypeScript migration path often ends up like: ```typescript import { Client as V1 } from "@acme/sdk-v1"; import { Client as V2 } from "@acme/sdk-v2"; ``` Which is clean… until your customer wants to use _both_ because they're migrating gradually. #### Variant: resource-based versioning `/api/users/v2/...` can be a pragmatic compromise when only one resource is being redesigned. But it can also create a patchwork API where every resource has its own version story. --- ### Opt-in features (client-side feature flagging) Instead of "pick a version", clients say "I want feature X". For example: - `X-Features: view_details, paused_status` **Pros** - Maximum flexibility. - Clients can pick and mix. - Makes rollouts very transparent. **Cons** - You push cognitive load onto customers: they must know which knobs to set. - You end up maintaining feature negotiation logic forever. Go example: ```go func hasFeature(r *http.Request, f string) bool { raw := r.Header.Get("X-Features") for _, part := range strings.Split(raw, ",") { if strings.TrimSpace(part) == f { return true } } return false } if hasFeature(r, "view_details") { // include new field } ``` TypeScript fetch example: ```typescript await fetch("/payees/p_123", { headers: { "X-Features": "view_details" }, }); ``` And if you're shipping an SDK, you can make this nicer: ```typescript const sdk = new Client({ features: ["view_details"] }); ``` --- ### Hybrid approach: version → feature resolution layer → feature-driven backend logic Combine explicit versions with internal feature flags. This is the "separation of concerns" approach: 1. Resolve a client property, e.g.: `version` into a feature set 2. Backend code asks "is feature X enabled?" not "is version ≥ 12.3.0?" ```go // Version resolution layer func resolveFeatures(version string) []string { switch { case version >= "2024-11-01": return []string{"enhanced-profiles", "new-permissions", "v2-pagination"} case version >= "2024-06-01": return []string{"enhanced-profiles", "new-permissions"} case version >= "2024-01-01": return []string{"enhanced-profiles"} default: return []string{} } } // Feature table at a glance // Version | enhanced-profiles | new-permissions | v2-pagination // 2024-01-01 | ✓ | | // 2024-06-01 | ✓ | ✓ | // 2024-11-01 | ✓ | ✓ | ✓ ``` **Pros** - Backend stays readable. It only deals with features, not versions - The resolution layer documents version evolution at a glance - Easier to test. Features names are easier to reason about, so tests become more readable. **Cons** - More infrastructure to maintain - Changes may require deploying multiple services - The mapping layer becomes critical path This is the strategy I usually prefer for public APIs because it makes compatibility an explicit subsystem, not an ad-hoc habit. --- ### Immutable deployments + dynamic routing This is the "every version lives forever" strategy. The simplest mental model is: **a version is a deployment**, not a branch of code. Vercel is a good reference point here: they [explicitly describe each push producing a new unique URL and a new immutable deployment](https://vercel.com/blog/framework-defined-infrastructure). They also document that generated deployment URLs remain accessible based on your retention policy. Use immutable deployments as a foundation for safe rollout strategies like blue/green. ``` # Each version maps to a distinct deployment api-v1.example.com -> deployment-abc123 api-v2.example.com -> deployment-def456 api-v3.example.com -> deployment-ghi789 ``` If you apply that idea to APIs, you get: - `deployment_id` (or `commit_sha`) becomes the version key - routing layer maps that key to the correct deployment **Pros** - No "compatibility code" inside a deployment — each one behaves consistently. - SDK versions can align 1:1 with API versions. - Rollbacks are easy (just route traffic differently). **Cons** - Security patching becomes painful. If old deployments still exist, you need a policy for patching or killing them. - You risk giving customers no incentive to upgrade. - Database schema changes become complex (which schema version does each deployment use?) - Shared resources (queues, caches) need to handle multiple API versions simultaneously If you go down this route, borrow from [immutable infrastructure thinking: AWS describes immutable infrastructure](https://docs.aws.amazon.com/wellarchitected/latest/framework/rel_tracking_change_management_immutable_infrastructure.html) as "no in-place modifications", replacing resources on change to improve reliability and reproducibility. That gives you a framework for why this works — and what operational controls you'll need. --- ### The "shove it under the rug" approach A translation layer sits in front: - requests come in - layer transforms them to what the backend understands - responses get transformed back to what the client expects This can be code-driven, config-driven, or (increasingly) "AI-driven". A simple architecture sketch: ```mermaid flowchart LR C[Client / SDK] -->|Request v=2024-01-01| T[Translation Layer] T -->|Canonical request| B[Backend] B -->|Canonical response| T T -->|Transformed response| C ``` **Pros** - Mostly plug and play – and it's just getting easier with AI-driven translation layers. - You can use this to build a "compatibility layer" for legacy APIs or third-party APIs. **Cons** - If you're hosting your own compatibility layer, or building your own translation layer, you're adding a lot of complexity to your architecture. - If you're using a third-party compatibility layer, you're adding cost and a new critical point of failure. - Some transformations cannot be expressed in a static configuration. - Can't express side-effects or conditional logic. My take: this is a reasonable last resort when you have low internal discipline or a messy legacy API. But if you have the chance to build a principled versioning model, do that instead. --- ## How to send version info You've basically got four knobs. None are perfect. | Mechanism | Example | Pros | Cons | | ------------------ | -------------------------------------- | ----------------------------------------------- | -------------------------------------------------------------- | | URL path | `/v1/users` | Visible, cache-friendly, easy routing | Version becomes "part of the URL", hard to evolve gradually | | Header (dedicated) | `X-Api-Version: 2024-06-01` | Explicit, easy to test, plays well with one URL | Some tooling hides headers; needs docs discipline | | Header (Accept) | `Accept: application/vnd.acme.v2+json` | Uses content negotiation semantics | Verbose, annoying in browsers, harder SDK ergonomics | | Query param | `?api_version=2024-06-01` | Easy to try manually | Often abused, sometimes cached badly, feels less "contractual" | If I'm building a serious public API today, I default to a **dedicated header**. It's explicit without turning versioning into URL sprawl. GitHub's REST API uses this exact pattern with `X-GitHub-Api-Version` and date-based versions. Stripe does the same conceptually with `Stripe-Version`. That's a pretty good signal that the ergonomics work at scale. --- ## How to sunset and deprecate Versioning without a lifecycle is just hoarding. You need: - monitoring (who is still using old versions?) - deprecation signals (tell them early) - a sunset plan (turn it off eventually, or you'll maintain it forever) - communication (docs, changelogs, email, dashboard banners) ### Use standard headers where possible There are now RFCs for this. - [**Deprecation** header (RFC 9745)](https://www.rfc-editor.org/rfc/rfc9745.html) communicates that a resource is (or will be) deprecated, and it carries a deprecation date. It's a structured header; the RFC example uses a Unix timestamp format like `@1688169599`. - [**Sunset** header (RFC 8594)](https://www.rfc-editor.org/rfc/rfc8594.html) communicates when a resource is expected to become unavailable (HTTP-date format). Example response headers: ```http Deprecation: @1767139200 Sunset: Wed, 31 Dec 2025 00:00:00 GMT Link: ; rel="deprecation"; type="text/html" ``` The RFC also calls out an important constraint: sunset shouldn't be earlier than deprecation. ### Have a process, not a hope A process sketch: ```mermaid flowchart TD A[Detect old version usage] --> B[Announce deprecation] B --> C[Add Deprecation header + docs link] C --> D[Add Sunset date] D --> E[Escalate comms: email + dashboards] E --> F[Block new signups on old versions] F --> G[Sunset: return 410 / hard fail] G --> H[Remove support + delete code paths] ``` And you need a support window. GitHub commits to supporting the previous REST API version for **at least 24 months** after a new one is released. That's a clear promise customers can plan around — and it forces internal discipline. How many breaking changes do you ship in a year? How about 2 years? Do you have the bandwidth to support all of those versions? --- ## How we help solve these issues at Speakeasy At Speakeasy, we help some of the most popular software platforms generate world-class SDKs that work seamlessly. Versioning, forward and backward compatible changes are always top-of-mind. You can [read more about how we implement forward compatibility here](https://www.speakeasy.com/docs/sdks/manage/forward-compatibility). ### SDK behaviour drift [David's TypeScript forward-compatibility article](https://www.speakeasy.com/blog/typescript-forward-compatibility) describes a painful user experience: server returns `200 OK`, SDK throws anyway. Causes include API evolution and inaccurate OpenAPI specs, and strict enum/union/required-field validation is a common trigger. The response is to build SDKs that degrade gracefully: - forward-compatible enums (accept unknown values in a type-safe way) - forward-compatible unions - "lax mode" for missing/mistyped fields - smarter union deserialisation strategies There's a difference between: - "SDK is a strict contract enforcer" - "SDK is a resilient integration tool" Most customers want the second. We support both. ### Explicit versioning and metadata propagation We use a hash of your full spec to track changes across versions, so you're not required to explicitly update your version to communicate changes. However, I do recommend **versioning your OpenAPI spec**. This allows you to make that version visible at runtime. In Speakeasy SDKs, you can [use hooks to inject cross-cutting behaviour](https://www.speakeasy.com/docs/sdks/customize/code/sdk-hooks) (headers, tracing, auth tweaks, etc.). A practical pattern is: - send OpenAPI doc version on every request - send SDK version on every request - use those values for feature negotiation and observability Illustrative TypeScript hook: ```typescript import { SDK_METADATA } from "../lib/config.js"; import { Hooks } from "./types.js"; export function initHooks(hooks: Hooks) { hooks.registerBeforeRequestHook({ beforeRequest(_, request) { request.headers.set( "x-openapi-doc-version", SDK_METADATA.openapiDocVersion, ); request.headers.set("x-sdk-version", SDK_METADATA.sdkVersion); return request; }, }); } ``` Why this helps: - **OpenAPI doc version** can map to feature sets (server-side) without guessing. - **SDK version distribution** tells you who's stuck, who upgrades, and which customers will be hurt by a breaking change. ```go func trackVersionMetrics(r *http.Request) { sdkVersion := r.Header.Get("x-sdk-version") openapiVersion := r.Header.Get("x-openapi-doc-version") // NOTE: you should validate the versions, so malicious or malfunctioning clients // don't bork your metrics system with high-cardinality columns metrics.Increment("api.requests", map[string]string{ "sdk_version": sdkVersion, "openapi_version": openapiVersion, }) } ``` You can't manage what you can't see. ### Maintaining backwards compatibility with continuous testing If you're not already doing this, don't worry. This is where most teams are weakest. They "try not to break stuff", but they don't continuously prove it. One solid approach is **workflow-based end-to-end tests**. If you haven't already, I recommend reading Brian's article on [Arazzo-based E2E testing](https://www.speakeasy.com/blog/e2e-testing-arazzo). It describes in great detail how we've implemented Arazzo-based E2E testing for our generated SDKs. A similar setup can enable your team to move more confidently across version changes. What makes this powerful, is that it's testing the things customers actually do. A practical model: 1. Keep historical versions of your OpenAPI spec (git tags are fine). 2. Keep Arazzo workflows for your critical customer journeys. 3. On every deploy (or nightly): - check out older spec versions - run the workflows against your current API - fail fast if you broke an older contract This turns backward compatibility from painful chore into an executable guarantee. --- ## Core principles I'd actually follow If you only remember a handful of things, make it these: 1. **Define the stable core of your API** Be explicit about what's truly contract, and what's best-effort. 2. **Treat "additive" changes as dangerous unless you've designed for resilience** Adding an enum value is dangerous (GitHub says so). Adding new fields is "backward-compatible" only if clients don't validate strictly (Stripe's docs assume this). 3. **Make changes opt-in where you can** New fields, new behaviours, new defaults — opt-in beats surprise. 4. **Versioning strategy is part of your interface forever** Pick something you're willing to support for years, not months. 5. **Have an explicit deprecation and sunset policy** Use runtime signals like Deprecation/Sunset headers, and back them with real comms. 6. **Harden observability around versions and features** You need to know who is on what, or you're flying blind. 7. **Automate backwards compatibility testing** Prefer workflow-level tests (Arazzo-style) over "unit tests of handlers", because customers don't call endpoints in isolation. And the meta-rule, courtesy of Hyrum's Law: **assume customers depend on everything you didn't mean to expose.** Once you accept it, you stop being surprised by breakages — and you start designing so they don't happen in the first place. # building-php-sdks Source: https://speakeasy.com/blog/building-php-sdks import { YouTube } from "@/mdx/components"; The ability to streamline and simplify the integration process between systems is getting more and more invaluable. Everything, and I mean everything, is starting to come with its own API. If we want to implement new features, the chances are we will need to work with and external API of some description. Sometimes they offer SDKs, sometimes they don't. The chances of them supporting your language, or framework, in the way that you need it …. Need I say more? So learning how to build an SDK in PHP is a skillset you should definitely consider picking up. If you are building your own API and want people to use your service, you will want to provide an SDK for them to use. In this tutorial, we are going to walk through the decisions you will take when designing an SDK in PHP: - [What are we building](#what-are-we-building) - [Thinking about Developer Experience](#thinking-about-developer-experience) - [Designing our Resources](#designing-our-resources) - [Working with Parameters](#working-with-query-parameters) - [Building our HTTP Request](#building-our-http-request) - [PSR-18 and sending HTTP Requests](#psr-18-and-sending-http-requests) - [Working with Responses](#working-with-responses) - [Pagination](#pagination) - [Sending data through our SDK](#sending-data-through-our-sdk) - [Summary](#summary) There are many ways to skin a cat, I mean build an SDK. One of the first questions you need to answer is how much opinionation you want to bake into your SDK at the expense of flexibility. My approach will be unopinionated about dependencies, but more opinionated when it comes to the architecture and implementation. That's because dependencies can be a sticking point for a lot developers, who may feel strongly about Guzzle vs. Symfony or have strict procedures in place for external dependencies. We want to ensure maximum compatibility with the PHP ecosystem. So we need to learn how to build SDKs that work no matter what. Now, let's walk through how we might go about building an SDK. ## What are we building? We are going to be building an SDK for a fictional e-commerce start up. The primary focus for their API is to allow their customers to sell products. Quite a common use case I am sure we can all agree. When it comes to building an SDK, the first thing you want to think about is access. What do you want to enable access to, what resources are going to be available, and how should this work. Do we want full access? Do we want partial access, maybe read only access? This is typically tied directly to the abilities of your API. For the SDK we are going to build, we want to be able to do the following: - List all products, allowing the filtering and sorting of results. - List a customers order history, and understanding the status of each order. - Allowing customers to start creating an order, and progressing it to payment. - Generate invoices for orders. We won't be handling any payment intents or actual payments in our fictional API, as there are enough payment providers out there already. At this point we want to start thinking about the configurable options that we might have in our SDK. We can pull out the resources from the list above with relative ease. We will then need an authentication token to be passed in so that our SDK can be authorized to perform actions for us. We will also want some level of store identifier, which will indicate a specific customer's account. How the API is set up, will depend on how the store identification works. We could use a subdomain identifier for our store, a query parameter, or a header value. It depends on how the API has been implemented. For this let's assume we are using the subdomain approach, as it is the most common method I have seen. ## Thinking about Developer Experience The DX is something that is important, frustrations with an SDK is the quickest way to lose the adoption you are trying to grow. Bad developer experience signals to developers that your focus isn't on making their lives easier. Some common things you should focus on that I find works well for developer experience are: - Ensuring compatibility with as many implementations as possible - Limiting third-party dependencies that could change behaviour, or break with updates - Handling serialization effectively, nobody wants a JSON string to work with - they want objects - Supporting pagination for paging through long result sets - Providing programatic control over filtering query parameters, This can tell us a lot about how to start our SDK, as we now know the parameters we need to pass to the constructor. The main thing we want to think about when it comes to our SDK, other than the core functionality, is developer experience. So let's start with some code, and I can walk you through the next steps: ```php declare(strict_types=1); namespace Acme; final readonly class SDK { public function __construct( private string $url, private string $token, ) {} } ``` At this point we have an SDK class that we can use to start integrating with. Typically what I like to do is test the integration as I am building, to make sure that I am not going to be creating any pain points that I can solve early on. ```php $sdk = new Acme\SDK( url: 'https://acme.some-commerce.com', token: 'super-secret-api-token', ); ``` This would typically be loaded in through environment variables and dependency injection, so we wouldn't construct the SDK directly very often. However, we cannot rely on the assumptions here. In Laravel this would be declared in the following way: ```php final class IntegrationServiceProvider extends ServiceProvider { public function register(): void { $this->app->singleton( abstract: Acme\SDK::class, concrete: fn () => new Acme\SDK( url: config('services.commerce.url'), token: config('services.commerce.token'), ), ); } } ``` We should always test this too, I know I know, why test a constructor? Honestly, it is more of a habit right now than anything. Getting into the practice of testing your code is never a bad thing! ```php it('can create a new sdk', function (): void { expect( new Acme\SDK( url: 'https://acme.some-commerce.com', token: 'super-secret-api-token', ), )->toBeInstanceOf(Acme\SDK::class); }); ``` As you can see here, I am using Pest PHP for testing. It's less verbose and I think it's actually fun to write! I find if you enjoy how you write tests, you are more likely to actually write the tests themselves. ## A Note on Authentication In the example above you'll notice that I am assuming that you will provide API Tokens for your users to use for their integrations. However, when it comes to APIs there are multiple options available. What is best depends on your usage patterns. You could use OAuth, HTTP Basic, API Token, or Personal Access Tokens. Each option has its benefits, depending on what you need to achieve and what you are providing. A great example use case of something like OAuth would be if your API or service is designed to be tightly controlled. The implementation is something that you do not want to share credentials with directly, instead you want to proxy the control of this to the service you are authenticating with, which then provides an Access Token that the SDK/implementation can use on the users behalf. Using HTTP Basic auth is something you see less and less of these days. It used to be extremely popular with government services, where you use you credentials directly to have access remotely. The core principle here is that the application doesn't care if it is a first or third party, they should all have the same level of control and access. That leaves API Tokens or Personal Access Tokens. This is my preferred method of authentication. You, as a user, create an API token that you want to use to gain access to the API. You can scope this to specific abilities and permissions, which then allows you to do exactly what you need nothing more. Each token is typically tied directly to a user, or entity. Using this token then also ties any actions you are trying to take directly to the entity the token belongs to. You can quickly and easily revoke these tokens, and you can cascade the deletion of the entity out to the tokens themselves. This is very similar to OAuth, but without as many hoops which makes it a great choice - at least until you actually need OAuth of course. ## Designing our Resources From our testing above, we know the instantiation works. What's next? Up to this point we have thought about the developer experience, and figured out how we want users to authenticate the SDK. Next we want to start defining the interface for our resources, starting with the Product resources. How I imagine this working is the following: ```php $sdk->products()->list(); // this should return a collection of products $sdk->products()->list('parameters'); // We should be able to filter based on parameters ``` To start building the Product resource out properly, we want to understand the potential options that we will be able to filter based on but also sorting. Personally I like enums for some of this, as it makes the most sense. Using an Enum allows you to tightly control what would be floating constants in your source code, it also gives you control over potential typos from the end user. ```php enum Sort: string { case Price = 'price'; case Age = 'created_at'; // other options you may want to sort on... } ``` This would be used like the following: ```php $sdk->products()->list( sort: Sort::price, direction: 'desc|asc', ); ``` This allows us to easily sort programmatically, giving as much control to the person implementing your SDK as possible. ## Working with Query Parameters So, filtering. Filtering is an interesting one. There are a few different approaches that we could take here, with no clear winner. The option I personally like is passing in a list of filters to iterate over: ```php $sdk->products()->list( filters: [ Filter::make( key: 'brand', value: 'github', ), ], ); ``` This allows us to programmatically build up our request exactly as we want it. In theory this is perfect, how about in practice though? Is this going to cause frustrations? ```php final readonly class IndexController { public function __construct( private SDK $sdk, private Factory $factory, ) {} public function __invoke(Request $request): View { $products = $this->sdk->products(); $sort = []; if ($request->has('sort')) { $sort['on'] = Sort::from( value: $request->string('sort')->toString(), ); $sort['direction'] = $request->has('direction') ? $request->string('direction')->toString() : 'desc'; } $filters = []; if ($request->has('filters')) { foreach ($request->get('filters') as $filter) { $filters[] = Filter::make( key: $filter['key'], value: $filter['value'], ); } } try { $response = $products->list( filters: $filters, sort: $sort['sort'] ?? null, direction: $sort['direction'] ?? null, ); } catch (Throwable $exception) { throw new ProductListException( message: 'Something went wrong fetching your products.', previous: $exception, ); } return $this->factory->make( view: 'pages.products.index', data: [ 'products' => $response, ], ); } } ``` So, this isn't perfect. We start by getting the products resource from the SDK, then process our request to programmatically change how we want to send the request to the API. Now, this is ok, but it is very long winded, which opens it up for issues and user error. We want to control things a little tighter, while still providing that flexibility. It does give me the approach I want and need to get exactly the data I want, but in a way that I personally wouldn't want to have to use. In reality this would have been wrapped in a Service class to minimize breaking changes. If we go with this approach, we can now start implementing the resource itself. ```php final readonly class ProductResource { public function __construct( private SDK $sdk, ) {} public function list(array $filters = [], null|Sort $sort = null, null|string $direction = null): Collection { // build initial request // build up request with filters and sorting // send request // capture response // throw error if response failed // return transformed response as a collection } } ``` For now I am just commenting the steps here, because we'll be stepping through each of these parts one by one. ## Building our HTTP Request Building up the initial request. We want to make sure that we aren't making any decisions for the user when it comes to their HTTP client. Luckily there is a PSR for that ([PSR-17](https://www.php-fig.org/psr/psr-17/)), which also allows us to leverage auto-discovery. All of our resources are going to be required to create requests to send. We could either create an abstract resource, or a trait. I personally prefer composition over inheritance, so I would typically lean towards a trait here. The main benefit of composition is that we know that each resource is going to implement similar functionality - however, if we need tighter control over just one thing we can partially pull in a trait or simply not use it. Also, when it comes to testing, testing traits is a lot easier than testing abstract classes. ```php trait CanCreateRequests { public function request(Method $method, string $uri): RequestInterface { return Psr17FactoryDiscovery::findRequestFactory()->createRequest( method: $method->value, uri: "{$this->sdk->url()}/{$uri}", ); } } ``` This trait allows us to access the discovered Request Factory that implements PSR-17, to then create a request using a passed in method and url. The method here is a simple Enum that allows programatic choice of method, instead of using static variables like `GET` or `POST`. As you can see we need to extend our base SDK class right now, to provide accessors to the private properties of `url` and later on `token`. ```php final readonly class SDK { public function __construct( private string $url, private string $token, ) {} public function url(): string { return $this->url; } public function token(): string { return $this->token; } } ``` The first step is done, we can now create the request we need to so that we can build the request as required. The next step is to add the trait to the resource class, so we can implement the required logic. ```php final readonly class ProductResource { use CanCreateRequests; public function __construct( private SDK $sdk, ) {} public function list(array $filters = [], null|Sort $sort = null, null|string $direction = null): Collection { $request = $this->request( method: Method::GET, uri: '/products', ); // build up request with filters and sorting // send request // capture response // throw error if response failed // return transformed response as a collection } } ``` As you can see from the above, to build the request all we need to do is interact with the trait we have created. This will use the PSR-17 factory discovery to find the installed Request Factory, and create the request based on the parameters we sent through. The chances are that we will want to build up our requests in a lot of our resources, so we will need to extend our trait. ```php trait CanCreateRequests { public function request(Method $method, string $uri): RequestInterface { return Psr17FactoryDiscovery::findRequestFactory()->createRequest( method: $method->value, uri: "{$this->sdk->url()}/{$uri}", ); } public function applyFilters(RequestInterface $request, array $filters): RequestInterface { foreach ($filters as $filter) { // now we need to work with the filter itself } } } ``` But, before we work with the filters on the request we need to understand the options for the filters. They are using query parameters to build the query parameters, which is supported in PSR-7. Let's look at the filter class, and add a method for working with the filters. ```php final readonly class Filter { public function __construct( private string $key, private mixed $value, ) {} public function toQueryParameter(): array { return [ $this->key => $this->value, ]; } public static function make(string $key, mixed $value): Filter { return new Filter( key: $key, value: $value, ); } } ``` We just need a way to take the content passed into the filter class, and turn it into an array that we can work with. ```php trait CanCreateRequests { public function request(Method $method, string $uri): RequestInterface { return Psr17FactoryDiscovery::findRequestFactory()->createRequest( method: $method->value, uri: "{$this->sdk->url()}/{$uri}", ); } public function applyFilters(RequestInterface $request, array $filters): RequestInterface { $parameters = $request->getQueryParameters(); foreach ($filters as $filter) { $parameters = array_merge($parameters, $filter->toQueryParameter()); } return $request->withQueryParameters($parameters); } } ``` In the snippet above, we are extracting the query parameters that may already be in place on our request, then merging them with the passed through filter query parameters, before returning back our modified request. A thing to note here is that the PSR-7 request is typically immutable by default, so you need to make sure that your logic is applied in the way that you expect. ```php final readonly class ProductResource { use CanCreateRequests; public function __construct( private SDK $sdk, ) {} public function list(array $filters = [], null|Sort $sort = null, null|string $direction = null): Collection { $request = $this->request( method: Method::GET, uri: '/products', ); $request = $this->applyFilters( request: $request, filters: $filters, ); // send request // capture response // throw error if response failed // return transformed response as a collection } } ``` We can now work on sending the request, and how we might want to achieve that using PSRs too. ## PSR-18 and sending HTTP Requests We've already seen PSR-17, but how about [PSR-18](https://www.php-fig.org/psr/psr-18/). It allows a level of interoperability between HTTP clients, so that you aren't stuck using Guzzle v7.0 or Symfony HTTP Client. Instead, like all good software, you build your implementation to an interface and rely on dependency injection or similar to tell the application exactly what should be used when resolving the interface. This is clean, very testable, and great for building PHP code that isn't going to break from a random `composer update`. How can we implement it though? It can be pretty confusing to try and implement PSRs on their own, the documentation is aimed at library authors who are typically used to reading specification documents. Let's look at a quote from the specification so you can understand what I mean. > Note that as a result, since [PSR-7 objects are immutable](https://github.com/php-fig/fig-standards/blob/master/accepted/PSR-7-http-message-meta.md#why-value-objects), the Calling Library MUST NOT assume that the object passed to `ClientInterface::sendRequest()` will be the same PHP object that is actually sent. For example, the Request object that is returned by an exception MAY be a different object than the one passed to `sendRequest()`, so comparison by reference (===) is not possible. Now if you read it, it makes sense! But if you are trying to build against it, along with other PSRs - things can get complicated quickly as the rules pile up in-front of you. This is why we use a library such as [PHP HTTP](https://docs.php-http.org/en/latest/), which allows us to auto-discover everything that we need. Using this library, we are able to discover the HTTP client installed and use it directly. However, I prefer (and recommend) a different approach. The [PHP-HTTP library](https://docs.php-http.org/en/latest/) offers a Plugin Client that we can use. This offers more of a composable approach to building up our HTTP client. Let's look at how we can use this in isolation before how we might implement this into our SDK. ```php use Http\Discovery\HttpClientDiscovery; use Http\Client\Common\PluginClient; $client = new PluginClient( client: HttpClientDiscovery::find(), plugins: [], ); ``` So, our plugin client will use the discovered client, but also accept an array of plugins the can be applied on each request. You can see all of the requirements for this [here](https://docs.php-http.org/en/latest/plugins/introduction.html) but I will walk you through a standard approach that I like to use: ```php use Http\Discovery\HttpClientDiscovery; use Http\Client\Common\PluginClient; use Http\Client\Common\Plugin\AuthenticationPlugin; use Http\Client\Common\Plugin\ErrorPlugin; use Http\Client\Common\Plugin\RetryPlugin; use Http\Message\Authentication\Bearer; $client = new PluginClient( client: HttpClientDiscovery::find(), plugins: [ new RetryPlugin(), new ErrorPlugin(), new AuthenticationPlugin( authentication: new Bearer( token: 'YOUR_API_TOKEN', ), ), ], ); ``` You can add as many plugins as you need here, there are cache plugins, history plugins, decoding plugins. The list goes on, and is well documented in case you want to build your own plugins. But retry, errors, and authentication is a good list to start with. Now we know how to build it, we can look at how we might want to implement this. All of our resources are going to want to send requests. But, we don't want to overload them with traits. To me, the perfect place for this is on the client itself. ```php final class SDK { public function __construct( private readonly string $url, private readonly string $token, private array $plugins = [], ) {} public function withPlugins(array $plugins): SDK { $this->plugins = $plugins; return $this; } public function url(): string { return $this->url; } public function token(): string { return $this->token; } } ``` First we need to start by removing the `readonly` from the class, and add it to the `url` and `token` properties. The reason for this is because our plugins property that we need to add, we want to be able to override, or at least have the option to should we need to. As this will be your SDK, we can customize this a little past this point. ```php final class SDK { public function __construct( private readonly string $url, private readonly string $token, private array $plugins = [], ) {} public function withPlugins(array $plugins): SDK { $this->plugins = array_merge( $this->defaultPlugins(), $plugins, ); return $this; } public function defaultPlugins(): array { return [ new RetryPlugin(), new ErrorPlugin(), new AuthenticationPlugin( new Bearer( token: $this->token(), ), ), ]; } public function url(): string { return $this->url; } public function token(): string { return $this->token; } } ``` Our final step is to have a way to get and override the client that will be used to send all of the HTTP requests. At this point our client is getting pretty big, and people using our SDK may want to implement their own approach. It is important that we avoid making too many decisions for our users. The best way to achieve this, as always, is to code to an interface. Let's design that now: ```php interface SDKContract { public function withPlugins(array $plugins): SDKContract; public function defaultPlugins(): array; public function client(): ClientInterface; public function setClient(ClientInterface $client): SDKContract; public function url(): string; public function token(): string; } ``` Let's focus in on our two new methods: ```php final class SDK implements SDKContract { public function __construct( private readonly string $url, private readonly string $token, private ClientInterface $client, private array $plugins = [], ) {} public function client(): ClientInterface { return new PluginClient( client: HttpClientDiscovery::find(), plugins: $this->defaultPlugins(), ); } public function setClient(ClientInterface $client): SDKContract { $this->client = $client; return $this; } } ``` Our `client` method will return a new Plugin Client, using HTTP discovery to find the client we have installed, also attaching the plugins that we want by default. But, how about if our users want to add additional plugins? How would this look in Laravel? ```php final class AppServiceProvider extends ServiceProvider { public function register(): void { $this->app->singleton( abstract: SDKContract::class, concrete: fn () => new SDK( url: config('services.commerce.url'), token: config('services.commerce.token'), client: new PluginClient( client: HttpClientDiscovery::find(), ), plugins: [ new CustomPlugin(), ], ), ); } } ``` ## Working with Responses At this point we are able to send the requests we need - at least for GET requests so far. Next, we want to look at how we receive and process the response data itself. There are two different approaches you could take when it comes to handling responses coming back from your API. - Return the PSR-7 Response directly - Transform the response into a Data Transfer Object. There are benefits to each approach, however it mostly depends on the purpose of the SDK. If you want a completely hands free approach, then working with Data Transfer Objects is my recommended approach. Providing strongly typed, contextual objects that your customers can use to work with the data as required. The other option is of course to allow the clients to transform the response, as they see fit. At this point we need to think back to what this SDK is for. This is an SDK that allows people to integrate with their online stores, so you want to be able to give as much freedom on implementation as possible. However for the purpose of education, let's show how we might transform this response data anyway. The way we do this in PHP is by designing our class, and hydrating this as we get the response. A fantastic resource for this is a package called [Serde](https://github.com/Crell/Serde) by a member of PHP-FIG, [Crell](https://github.com/Crell). Another option is to use [Object Mapper](https://github.com/thephpleague/object-mapper) which is by The PHP League. Both libraries offer a very similar functionality, the choice is more down to your personal preference. Our first step is to design the class we want to hydrate, this will typically match your API response. ```php final readonly class Product { public function __construct( public string $sku, public string $name, public string $description, public int $price, ) {} } ``` This assumes that any routing is using the `sku` to lookup the product. The way I like to use these objects is to add a static method that will hydrate the class. The reason for this is because it keeps all logic about creating the object is contained within the class it is creating. There is no looking around for what class does what, it is all in one place. ```php final readonly class Product { public function __construct( public string $sku, public string $name, public string $description, public int $price, ) {} public static function make(array $data): Product { $mapper = new ObjectMapperUsingReflection(); return $mapper->->hydrateObject( className: self::class, payload: $data, ); } } ``` As you can see, this is a neat bundle that will allow you to just send data to the class and receive the object back in a uniform way. How does this look in our SDK? ```php final readonly class ProductResource { use CanCreateRequests; public function __construct( private SDK $sdk, ) {} public function list(array $filters = [], null|Sort $sort = null, null|string $direction = null): Collection { $request = $this->request( method: Method::GET, uri: '/products', ); $request = $this->applyFilters( request: $request, filters: $filters, ); try { $response = $this->sdk->client()->sendRequest( request: $request, ); } catch (Throwable $exception) { throw new FailedToFetchProducts( message: 'Failed to fetch product list from API.', previous: $exception, ); } return new Collection( collectionType: Product::class, data: array_map( callback: static fn (array $data): Product => Product::make( data: $data, ), array: (array) json_decode( json: $response->getBody()->getContents(), associative: true, flags: JSON_THROW_ON_ERROR, ), ), ); } } ``` This attempts to send the request, and catches any potential exceptions. We then throw a contextual exception so that if anything does go wrong, we capture it and understand exactly what and where things broke. We then return a collection of Products, by mapping over the json response as an array. We are using the [Collection](https://github.com/ramsey/collection) library that is built by [Ben Ramsey](https://github.com/ramsey) here, not the Laravel one. We _could_ just return an array, but I find it useful if you are going to go to the effort of returning objects, wrapping them in something with additional developer experience is a huge plus. ## Pagination At some point you will need to make a decision about how you want to handle pagination - if at all. Let's walk through the options, and figure out the what why and hows for paginating your API requests in your SDK. The first option that most developers reach for is the `do while` approach. Which you add a do while loop within your code to just crawl the API endpoint until you get to the end of the paginated data - and then return the response. Personally I do not like this approach as it makes a few too many decisions for you. What if you don't want to fetch all of the data, and just want to first page? Next up, the paginator class. This will do almost the same as the do while approach, but instead you wrap the SDK call inside a pagination class which will handle the looping for you. This is a little better, as you aren't mixing the HTTP calls with client intended logic. However to achieve this, you need to add a way to work with pages within your methods. Finally, the programmatic approach. Much like the paginator class, your method will just accept a nullable page parameter which will request the specific page you actually want. Personally, I like this approach the most. If the client wants to paginate over the data, they have the ability to - without me forcing them into my way of doing it. Let's have a look at a quick example. ```php final readonly class ProductResource { use CanCreateRequests; public function __construct( private SDK $sdk, ) {} public function list( array $filters = [], null|Sort $sort = null, null|string $direction = null, null|int $page = null, ): Collection { $request = $this->request( method: Method::GET, uri: '/products', ); $request = $this->applyFilters( request: $request, filters: $filters, ); if (null !== $page) { $request = $request->withQueryParameters([ 'page' => $page ]); } // send request // capture response // throw error if response failed // return transformed response as a collection } } ``` If we pass through a page, we want to make sure we include it in the query parameters being sent over to the API. Your pagination may be different, for example you may use cursor pagination which will require you to pass over a specific hash. Yes the method parameters are getting long, but they all serve a purpose for control. Whoever said methods shouldn't have more than 3 arguments has never built an SDK before. On the client side, this is now simple to work with: ```php $products = $sdk->products()->list( page: 1, ); ``` You could even wrap this in your own pagination class or provide a dedicated one with your SDK should you need it. I will show a quick high level interface so you know how this would be structured. ```php interface Paginator { public function fetch(SDK $sdk, string $method, array $parameters = []): Generator; public function hasNext(): bool; } ``` Now, let's look at an implementation: ```php final class Pagination implements Paginator { public function __construct( private readonly int $perPage, private array $pagination = [], ) {} public function fetch(SDK $sdk, string $method, array $parameters = []): Generator { foreach ($this->fetch($sdk, $method, $parameters) as $value) { yield $value; } while ($this->hasNext()) { foreach ($this->fetchNext() as $value) { yield $value; } } } public function hasNext(): bool { return $this->pagination['next'] !== null; } private function get(string $key): array { $pagination = $this->pagination[$key] ?? null; if ($pagination === null) { return []; } // Send the request and get the response. $content = ResponseMediator::getContent($response); if (! \is_array($content)) { throw new RuntimeException('Pagination of this endpoint is not supported.'); } $this->postFetch(); return $content; } private function postFetch(): void { // Get the last request from the SDK Client. $this->pagination = $response === null ? [] : ResponseMediator::getPagination($response); } } ``` You do of course so something a little simpler if you need to, but in general this should work for you. The Response Mediator class is a utility class that I would sometimes use to simplify the working with API data. Let's move onto how we might actually send some requests now though. ## Sending Data through our SDK One of the final stepping stones to building a good SDK, is figuring out how we want to create and update potential resources. In our example of an e-commerce API, the likelihood of creating a product object via API is extremely low. Typically you would use a provided admin dashboard. So, for this next example we are going to focus on the Customer resource. When a user registers through your platform, you want to create a customer resource on the e-commerce API, so that if the authenticated user orders anything - they will be able to link to the correct customer quickly and easily. We will look at creating a new customer next. There are a few options, as always, when creating resources through an SDK. You can either: - Send a validated array through to the SDK - Send another Data Transfer Object specific to the request through to the SDK My personal preference here is to use DTOs and then let the SDK handle sending this in the correct format. It allows a more strongly typed approach, and puts all of the control in the hands of the SDK - which minimizes potential risk. ```php final readonly class CreateCustomer { public function __construct( public string $name, public string $email, public string $referrer, ) {} public static function make(array $data): CreateCustomer { $mapper = new ObjectMapperUsingReflection(); return $mapper->->hydrateObject( className: self::class, payload: $data, ); } } ``` Just like the Product DTO, we add a static make method using the object mapper to create the object itself. Let's now design the resource. ```php final readonly class CustomerResource { use CanCreateRequests; public function __construct( private SDK $sdk, ) {} public function create(CreateCustomer $customer) { $request = $this->request( method: Method::POST, uri: '/customers', ); // attach the customer as a stream. } } ``` We now need to work with our trait again, so that we can work with sending and using data. ```php trait CanCreateRequests { // Other method... public function attachPayload(RequestInterface $request, string $payload): RequestInterface { return $request->withBody( Psr17FactoryDiscovery::findStreamFactory()->createStream( content: $payload, ); ); } } ``` What we are doing here is passing through the request we are building, and the stringified version of the payload. Again, we can use auto-discovery to detect what HTTP Stream Factory is installed, then create a stream from the payload and attach it to the request as its body. We need a way to quickly and easily serialize the data from our DTOs to send through to create a stream. Let's look at the DTO for creating a customer again. ```php final readonly class CreateCustomer { public function __construct( public string $name, public string $email, public string $referrer, ) {} public function toString(): string { return (string) json_encode( value: [ 'name' => $this->name, 'email' => $this->email, 'referrer' => $this->referrer, ], flags: JSON_THROW_ON_ERROR, ); } public static function make(array $data): CreateCustomer { $mapper = new ObjectMapperUsingReflection(); return $mapper->->hydrateObject( className: self::class, payload: $data, ); } } ``` Now let's go back to the implementation. ```php final readonly class CustomerResource { use CanCreateRequests; public function __construct( private SDK $sdk, ) {} public function create(CreateCustomer $customer) { $request = $this->request( method: Method::POST, uri: '/customers', ); $request = $this->attachPayload( request: $request, payload: $customer->toString(), ); try { $response = $this->sdk->client()->sendRequest( request: $request, ); } catch (Throwable $exception) { throw new FailedToCreateCustomer( message: 'Failed to create customer record on the API.', previous: $exception, ); } // Return something that makes sense to your use case here. } } ``` So, we can quickly and easily create and send data. A typical usage of this in a Laravel application, would be to leverage the events system - listening for something like the `Registered` event to be fired: ```php final readonly class CreateNewCustomer { public function __construct( private SDK $sdk, ) {} public function handle(Registered $event): void { try { $this->sdk->customers()->create( customer: CreateCustomer::make( data: [ 'name' => $event->name, 'email' => $event->email, 'referrer' => $event->referrer, ], ), ); } catch (Throwable $exception) { Log::error('Failed to create customer record on API', ['event' => $event]); throw $exception; } } } ``` Quite clean and easy to use I am sure you would agree. The only improvement I would potentially suggest here is to use a dispatchable job or event-sourcing style system here, something that would allow you to replay the attempt - giving you the opportunity to fix and retry. ## Summary As you can see from this tutorial, building an SDK for your API isn't overly tricky - but there are a lot of things to think about. With developer experience being a key factor in the success of your SDK, you need to make sure you think about that alongside the technical requirements that your SDK has. At Speakeasy we have carefully designed how SDKs should work in each language we support, allowing you to follow a similar approach to the above without having to write a single line of code. Instead it will use your OpenAPI specification to generate a robust, well tested, and developer friendly SDK for your API. Even better, it will take less time than waiting for a pizza delivery. Now, I have always been against autogenerated SDKs, especially when you see some of the examples out there. However, what Speakeasy does is a completely different approach that guarantees better success and developer experience. Instead you can focus on building the best API and OpenAPI specification you can - and let us focus on providing you with a great SDK in multiple languages. # building-speakeasy-openapi-go-library Source: https://speakeasy.com/blog/building-speakeasy-openapi-go-library At Speakeasy, we process thousands of OpenAPI specifications every day across a wide range of teams and industries. These specs drive our production-ready SDKs, Terraform providers, and a growing set of internal tools. That volume exposes every sharp edge of OpenAPI. From small hand-written specs to multi-megabyte machine-generated schemas with deeply nested references and vendor extensions, we’ve seen almost every way an OpenAPI document can go wrong. As our platform grew, we hit the limits of existing Go libraries. Some were fast but modeled the spec loosely, making it hard to build correct tooling on top. Others were closer to the spec but used untyped maps everywhere, which made large refactors and static analysis painful. We needed something different: a library that could be both a precise model of the specification and a high-performance engine for reading, validating, mutating, and transforming specs at scale. So we built our own. Today, we’re introducing [`github.com/speakeasy-api/openapi`](https://github.com/speakeasy-api/openapi), a comprehensive set of packages and tools for working with OpenAPI, Swagger, Arazzo, and Overlay Specification documents. While our [release post](/blog/release-oss-openapi-library) covers the high-level features, this article focuses on the engineering: the core abstractions, performance decisions, and tradeoffs that make this library a strong foundation for any OpenAPI tooling written in Go. If you're choosing an OpenAPI library for Go today, our goal is that this post gives you enough signal to make this your default choice. ## The Challenge: OpenAPI is Hard OpenAPI is a deceptively complex specification. It has evolved significantly over time (2.0 to 3.2), supports dynamic types (fields that can be a string or an array), and relies heavily on JSON Schema, which allows for recursive and circular references. Existing Go libraries often struggle with these edge cases, either by simplifying the model (losing accuracy) or by exposing a raw, untyped map structure (losing type safety). We needed a solution that offered both **correctness** and **developer experience**. In practice, these tradeoffs show up as: - Tools that silently drop parts of the spec when they encounter constructs they don’t model. - Validators that can’t follow complex or circular `$ref` graphs and either blow up or behave inconsistently. - Libraries that are easy to start with, but brittle to extend when a new OpenAPI minor version or companion spec (like Arazzo or Overlays) appears. Our goal for this library was to make those classes of bugs structurally harder to introduce by encoding more of the spec’s rules and invariants directly into the type system and core architecture. ## Architecture: The Reflection-Based Marshaller One of the core design decisions we made was to build the library on top of a custom, reflection-based marshaller. In many libraries, the logic for parsing JSON/YAML is tightly coupled with the struct definitions. This makes it hard to support multiple specification versions or new specs like Arazzo without duplicating a lot of boilerplate code. Our approach separates the **model definition** from the **deserialization logic**. We define our Go structs to match the specification as closely as possible, and our marshaller handles the complexity of mapping the input data to these structs. This allows us to: 1. **Iterate fast:** We can add support for new specs (like we recently did with Swagger 2.0) by simply defining the structs, without writing bespoke parsing logic. 2. **Optimize centrally:** Performance improvements in the marshaller benefit all supported specifications immediately. Under the hood, the marshaller walks a graph of Node values (an internal intermediate representation of the raw YAML/JSON) produced from the original YAML/JSON document. Instead of binding directly to concrete structs at parse time, we keep a lightweight intermediate representation that preserves: - The original shape of dynamic fields (single value vs array, inline vs `$ref`). - Location information that we can use in validation errors. - Enough metadata to support additional specifications without rewriting the core. When we finally bind into Go structs, we do so using a set of small, reusable reflection helpers that know how to: - Map OpenAPI/JSON Schema primitives and unions into strongly typed fields. - Apply defaulting and normalization rules in one place. - Reuse the same code paths across OpenAPI 3.x, Swagger 2.0, Arazzo, and Overlays. This architecture means that adding support for a new spec or version is mostly a matter of: - Defining new Go structs that closely mirror the specification. - Wiring them into the existing marshaller. The heavy lifting—parsing, node traversal, defaulting, and error reporting—remains centralized and battle-tested. ## Performance: Porcelain vs. Plumbing To handle "thousands of specs" efficiently, we adopted a "Porcelain vs. Plumbing" API design. - **Plumbing:** The internal representation is optimized for efficient storage and iteration. We use well-defined conventions to standardize how data is stored, allowing us to minimize memory allocations during parsing. - **Porcelain:** The public API provides a clean, high-level interface for developers. You don't need to worry about the internal storage details; you just interact with idiomatic Go structs. This separation allows us to optimize the "hot paths" of serialization and deserialization without breaking the user-facing API. On the plumbing side, we optimized for the workloads we see most often at Speakeasy: repeatedly parsing and transforming large specs as part of CI pipelines, code generation, and analysis tools. Some of the concrete decisions we made here include: - Preferring stable internal representations that can be reused across passes (validation, traversal, mutation) rather than re-parsing. - Minimizing allocations in hot paths inside the marshaller and walker. - Designing APIs that compose naturally with Go’s concurrency primitives so you can fan out work across operations, paths, or components when it makes sense for your use case. Because the public API is intentionally “porcelain,” these optimizations are mostly invisible to library consumers—but they matter when you’re processing thousands of specs or very large documents. ## Taming Dynamic Types with Type Safety One of the hardest parts of modeling OpenAPI in Go is the dynamic nature of the spec. For example, in OpenAPI 3.1, the `type` field of a schema can be a single string (e.g., `"string"`) or an array of strings (e.g., `["string", "null"]`). In a statically typed language like Go, this is usually handled by using `interface{}` (which loses type safety) or complex pointer logic. We introduced generic abstractions like `EitherValue` to handle these cases elegantly. For example, the `Type` field in our Schema struct is defined as: ```go // Type represents the type of a schema either an array of types or a single type. Type = *values.EitherValue[[]SchemaType, []marshaller.Node[string], SchemaType, string] ``` This abstraction allows us to capture the exact state of the document—whether it was defined as a single value or an array—while still providing type-safe accessors to the underlying data. Similarly, we use `JSONSchema[Referenceable]` to handle the complexity of JSON Schema references, ensuring that we can model both inline definitions and `$ref` pointers consistently. The key benefit isn’t just that we can represent more of the spec faithfully—it’s that the representation is consistent. The same patterns we use for the `type` field also show up with other dynamic fields and referenceable structures. That consistency makes the library predictable: - Once you know how to work with a value that may be a single item or a list, you can apply the same approach everywhere. - Tooling like IDEs and linters can understand your data flow because everything is strongly typed. - Refactors are safer because more invariants are enforced at compile time instead of being left to runtime checks or comments. ## Reference Resolution and Validation at Scale Correctly handling `$ref` pointers is one of the hardest parts of working with OpenAPI and JSON Schema in practice. Real-world specs frequently contain: - Deeply nested internal references. - References that cross between files. - Circular graphs that are valid but tricky to traverse safely. The library’s reference resolution engine is built on a few principles: - **Single source of truth for documents:** We maintain a document graph in memory that tracks where each node came from (file, path, and location). - **Stable identifiers:** Every referenceable element can be addressed via a stable pointer, making it easy to traverse and manipulate the graph. - **Separation of loading and validation:** We can first build the document graph, then apply multiple passes of validation without reloading or reparsing. This design lets us: - Resolve complex reference graphs without blowing the stack. - Emit useful error messages that point back to the exact location in the original document. - Compose operations like bundling, inlining, or overlay application on top of the same core engine. For example, if a circular reference chain is invalid because a required property is missing deep in the graph, the error message still points back to the exact `$ref` and location in the original file where the problem originates. ## Unified Ecosystem: Arazzo and Overlays Because we built a flexible core, we were able to extend the library to support [Arazzo](https://www.speakeasy.com/openapi/arazzo) (for workflows) and [Overlays](https://spec.openapis.org/overlay/latest.html) (for modifications) natively. Crucially, these packages share the same underlying models for common elements like JSON Schema and references. This means you can parse an OpenAPI spec, apply an Overlay to it, and then validate it against an Arazzo workflow, all within the same memory space and using the same tooling. We deliberately avoided creating separate, siloed models for each specification. Instead, Arazzo, Overlays, and OpenAPI all share a small set of core building blocks—JSON Schema, references, and common metadata structures. That means investments in those shared pieces (better validation, richer error messages, performance improvements) automatically benefit the entire ecosystem. If a new spec builds on the same foundations, we can usually support it without re-architecting the library. ## Key Benefits If you're working with OpenAPI in Go, here's why you should consider using this library: - **Full Version Support:** It supports OpenAPI 3.0.x, 3.1.x, and 3.2.x, along with Swagger 2.0, Arazzo, and Overlays—all in one place. - **Robust Reference Resolution:** Handling `$ref` pointers correctly is notoriously difficult. Our library provides a robust **reference resolution** engine that handles circular references and external files with ease. - **Idiomatic & Safe Go API:** The object models match the structure of the specifications as closely as possible. We prioritized nil safety and high-level APIs to reduce the need for diving into low-level details. - **Battle-Tested:** This library powers the Speakeasy platform, meaning it's tested against a vast array of real-world specifications. ## Powerful CLI Tooling Beyond the library, we provide a comprehensive CLI tool that exposes many of the library's capabilities directly to your terminal. It's packed with utilities to help you manage your API lifecycle: - **`bundle`**: Bundle external references into a single file. - **`inline`**: Inline all references to create a self-contained document. - **`overlay`**: Apply, compare, and validate OpenAPI Overlays to modify your specs without changing the source. - **`optimize`**: Deduplicate schemas and optimize your document structure. - **`sanitize`**: Remove unused components and clean up your spec. - **`snip`**: Extract specific operations or paths into a new document. - **`explore`**: Interactively explore your OpenAPI document in the terminal. Importantly, the CLI is a thin layer over the same Go packages you use in code. Every subcommand is built from the same primitives: parsing, walking, reference resolution, and mutation of the in-memory document graph. That means if you start by using the CLI for quick experiments—bundling, inlining, sanitizing—you can later pull the exact same operations into your own Go programs or CI pipelines with very little glue code. ## Getting Started Here are a few complete examples of how you can use the library to read, validate, mutate, and upgrade OpenAPI documents. ### Reading and Validating Reading a document is simple, and validation happens automatically by default. ```go package main import ( "context" "fmt" "os" "github.com/speakeasy-api/openapi/openapi" ) func main() { ctx := context.Background() f, err := os.Open("openapi.yaml") if err != nil { panic(err) } defer f.Close() // Unmarshal and validate doc, validationErrs, err := openapi.Unmarshal(ctx, f) if err != nil { panic(err) } // Check for validation errors if len(validationErrs) > 0 { for _, err := range validationErrs { fmt.Println(err.Error()) } } fmt.Printf("API Title: %s\n", doc.Info.Title) } ``` ### Traversing with the Walker The library provides a powerful iterator pattern to traverse the document, allowing you to inspect specific elements without writing complex recursive loops. This is useful for auditing all operations or programmatically curating a spec—the same walker that powers many of the CLI commands. ```go package main import ( "context" "fmt" "os" "github.com/speakeasy-api/openapi/jsonschema/oas3" "github.com/speakeasy-api/openapi/openapi" ) func main() { ctx := context.Background() f, err := os.Open("openapi.yaml") if err != nil { panic(err) } defer f.Close() doc, _, err := openapi.Unmarshal(ctx, f) if err != nil { panic(err) } // Walk through the document for item := range openapi.Walk(ctx, doc) { err := item.Match(openapi.Matcher{ Operation: func(op *openapi.Operation) error { if op.OperationID != nil { fmt.Printf("Found Operation: %s\n", *op.OperationID) } return nil }, Schema: func(schema *oas3.JSONSchema[oas3.Referenceable]) error { if schema.IsSchema() { fmt.Printf("Found Schema\n") } return nil }, }) if err != nil { panic(err) } } } ``` ### Mutating a Document You can easily modify the document programmatically and marshal it back to YAML or JSON. This pattern is useful when you want to enforce organization-wide conventions—like injecting standard servers, headers, or tags—across many specs. ```go package main import ( "bytes" "context" "fmt" "os" "github.com/speakeasy-api/openapi/openapi" "github.com/speakeasy-api/openapi/pointer" ) func main() { ctx := context.Background() f, err := os.Open("openapi.yaml") if err != nil { panic(err) } defer f.Close() doc, _, err := openapi.Unmarshal(ctx, f) if err != nil { panic(err) } // Modify the title doc.Info.Title = "Updated API Title" // Add a new server doc.Servers = append(doc.Servers, &openapi.Server{ URL: "https://api.example.com/v2", Description: pointer.From("New Production Server"), }) // Write back to YAML buf := bytes.NewBuffer([]byte{}) if err := openapi.Marshal(ctx, doc, buf); err != nil { panic(err) } fmt.Println(buf.String()) } ``` ### Upgrading to OpenAPI 3.2.0 One of the most powerful features is the ability to automatically upgrade older specs to the latest version. This pattern works well in CI pipelines where you want to accept older specs (3.0.x or 3.1.x) at the edge, but standardize everything internally on 3.2.0 before running validation, code generation, or analysis. ```go package main import ( "context" "fmt" "os" "github.com/speakeasy-api/openapi/openapi" ) func main() { ctx := context.Background() f, err := os.Open("openapi.yaml") if err != nil { panic(err) } defer f.Close() doc, _, err := openapi.Unmarshal(ctx, f) if err != nil { panic(err) } // Upgrade from 3.0.x or 3.1.x to 3.2.0 upgraded, err := openapi.Upgrade(ctx, doc) if err != nil { panic(err) } if upgraded { fmt.Printf("Upgraded to version: %s\n", doc.OpenAPI) } } ``` ## Conclusion If you’re building serious OpenAPI tooling in Go—linters, documentation generators, gateways, test harnesses, or CI checks—our goal is for this to be the library you reach for first. We’ve invested heavily in correctness, type safety, and performance because we rely on it in production every day, and we’re committed to evolving it alongside the ecosystem. Check out the code on [GitHub](https://github.com/speakeasy-api/openapi) and let us know what you think! # c-general-availability-our-ai-enhanced-cli Source: https://speakeasy.com/blog/c-general-availability-our-ai-enhanced-cli import { Callout, ReactPlayer } from "@/lib/mdx/components"; The release of our C# SDK marks an important milestone for Speakeasy. Over half of the languages we support are now in GA! As always, a massive thank you to all our customers and users for their feedback and support. We couldn't have done it without you 🎉 And it goes without saying, we won't stop here. More to come very shortly! ## C# General Availability Microsoft acolytes and fintech employees rejoice! We're excited to announce that C# is now generally available. General availability means that the public interface is now stable, and every feature of Speakeasy's generation platform is accessible. A few of the highlights that have us excited include: - Configurable support for `.NET 5.X` and above - `Async`/`Await` support - OAuth2.0 support - Support for complex number types: - `System.Numbers.BigInteger` - `System.Decimal` - Strong-typing with IntelliSense support The full details can be found [here](/docs/sdk-design/csharp/methodology-csharp) ## Speakeasy Ask We're excited to announce the release of [Speakeasy Ask](/docs/speakeasy-cli/ask), our AI-enhanced CLI. Speakeasy Ask is a new feature that allows you to ask questions about your OpenAPI spec and SDKs without leaving our CLI. This feature is designed to make it easy to access the Speakeasy knowledge base wherever your work happens. No more side by side windows or switching between tabs. Just ask your question and get the answer you need.
## 🚢 Improvements and Bug Fixes 🐛 Based on the most recent CLI version: [**Speakeasy v1.277.4**](https://github.com/speakeasy-api/speakeasy/releases/tag/v1.277.4) 🚢 `ClientCredentialSecurityAccess` for combined security options\ 🚢 Postman generation in alpha\ 🚢 Improved performance of `speakeasy validate` by making sure validation runs resolve names ### Python 🐛 Improved handling of errors returned by the `after_error` hook \ 🚢 Added support for unions as errors \ 🚢 Ensure classes can't use `Undefined` reserved word in Python ### Golang 🚢 Added support for unions as errors ### Java 🚢 Support added for SDK Hooks\ 🐛 Applied workaround for `jackson-databind` boolean serialization issue\ 🐛 Removed `jsonpath` dependency if pagination is not configured\ 🐛 Removed redundant imports in usage snippets\ 🐛 Used wildcard generic types as input only\ 🚢 Added `SuppressWarnings` unchecked annotations, report unchecked in build ### C# 🐛 Fixed missing import for flattened operation parameters\ 🚢 Added support for injection of additional dependencies ### Typescript 🐛 Removed excess comma when templating SSE responses with headers\ 🐛 Computed accept types from success responses\ 🐛 Added better error messages for `content type` mismatches\ 🚢 Updated SDKs to use `zod 3.23.4`\ 🚢 JSR publishing available out of the box \ 🚢 Added support for unions as errors\ 🚢 Added handling of optional security when using client credentials auth ### Terraform 🐛 Arrays are now instantiated as empty slices to avoid null being sent over the wire # choosing-a-docs-vendor Source: https://speakeasy.com/blog/choosing-a-docs-vendor import { Table } from "@/mdx/components"; High-quality docs are the backbone of a positive developer experience. Whether you're building an API, an SDK, or a complex piece of software, accessible and well-structured documentation helps users adopt your product more quickly and keeps users engaged over time. The right docs tool is indispensable, helping you organize information clearly and making it easy to update and expand your documentation as your product evolves. As SDK providers, we have worked with users of every docs platform. In this post, we compare five popular documentation vendors: Mintlify, Scalar, Bump, ReadMe, and Redoc. We'll guide you through a simple API docs setup for each tool and share our impressions to help you decide which best suits your needs. _Disclaimer: While we have worked extensively with every platform, **Speakeasy does have a reseller relationship with Scalar**._ ## Matching docs tools to use cases Let's start with the highlights. The recent entrants to the docs market: Scalar, Mintlify, Bump outshine the established incumbents: Redocly, and Readme. There are good arguments for each depending on your use case. We'll explore the strengths and weaknesses of each tool in detail later in this post, but here's the high level breakdown.
{/* TODO: add review card */} {/* */}
{/* TODO: add review card */} {/* */}
{/* TODO: add review card */} {/* */}
### Feature Matrix
--- ## Evaluation criteria We focus on the following key aspects that matter most to development teams: - **One special feature:** What's the singular feature that sets each tool apart from the others. - **Ease of use:** How quickly you can get started, how steep the learning curve is, and what automation features are available to speed up the workflow. - **Theming & customization:** How much control you have over the look and feel of docs, including theming options, layout flexibility, and the ability to match your brand identity. - **Developer experience:** How well the tool integrates with existing tools, particularly its CI/CD support and version control capabilities. - **Support & pricing:** The boring, but important things to consider when choosing a docs tool. --- ### Scalar: Interactive API docs that developers love
![API reference](/assets/blog/choosing-a-docs-vendor/scalar/api-reference-preview.png)
#### One special feature: standalone API client What truly sets Scalar apart is that its API client isn't just embedded in the documentation—developers can download it as a standalone application for local development. It's like Postman++, transforming documentation from mere reference material into an actual development tool that supports the entire workflow. This creates a unique bridge between documentation and active development that other platforms don't offer. ![API client](/assets/blog/choosing-a-docs-vendor/scalar/native-api-client.png) #### Ease of Use Setup is straightforward with a simple "New Project" flow and WYSIWYG editor. The platform automatically processes OpenAPI documents to generate an interactive reference without requiring additional configuration. #### Theming & customization Scalar offers a set of clean unpinionated default themes with customization available to create branded documentation. Users can also upload their own custom CSS and JavaScript files to fully customize the look and feel of their documentation. The only major limitation is the lack of support for MDX for those companies that want to embed custom components within their documentation. #### Developer experience One thing that really sets Scalar apart is its array of API framework integrations. For code-first organizations that use frameworks like Django, FastAPI, and Express, Scalar provides seamless integration. Developers can generate documentation directly from their codebase with little additional configuration. If you host your OpenAPI spec at a static URL, Scalar also has automatic updates via URL fetching that makes updates frictionless. Scalar now offers robust CI/CD integration through the [Scalar Registry](https://guides.scalar.com/scalar/scalar-registry), supporting popular platforms like GitLab, GitHub Actions, and other continuous integration tools. This makes it easy to automatically deploy and version your documentation as part of your development workflow. #### Support and pricing You won't find better in terms of product for the price. Scalar offers three subscription tiers: - **Free:** Full features for public APIs - **Team ($12/user/month):** Private docs, team features, upcoming Speakeasy integration - **Enterprise (custom pricing):** Advanced security and support --- ### Mintlify: Seriously beautiful docs #### One Special Feature: AI-ready Mintlify is the provider best equipped to make your API accessible to LLMs. They generate vanilla `.md` files for every documentation page, and automatically generate an `llms.txt`. That makes it easy for LLMs to parse and understand your API documentation. #### Ease of Use Onboarding is exceptionally smooth, with a guided process from signup to repository creation. The local development workflow using the Mintlify CLI provides immediate feedback through localhost previews. Mintlify's OpenAPI integration is less robust than other documentation tools, with some opaque error messages, and incorrect validation failures. But these can usually be worked through. IN part this is because Mintlify is a comprehensive docs solution first, and an API reference solution second. #### Theming & customization This is where Mintlify shines. It has sleek default themes, but also offers extensive customization options for: - Styling (primary colors, theming) - Navigation structure - Content layouts Plus, it supports MDX. That means that you can create custom components and embed them directly into your documentation. #### Developer Experience Mintlify embraces developer-friendly workflows with: - Git-based content management - Local development with live previews - Integration with external services (analytics, SDK generators,etc.) Mintlify also offers a great developer experience for teams creating narrative-based documentation in addition to references. #### Support and pricing Mintlify is a more expensive offering with three subscription tiers: - **Free:** For individual hobbyists - **Pro: $150/month** - For small teams and startups - **Growth: $550/month** - For growing companies - **Enterprise: custom pricing** - Custom built packages based on your needs --- ### Bump: Built for scale
![API reference](/assets/blog/choosing-a-docs-vendor/bump/api-reference.png)
#### One Special Feature: Performance at Scale Bump's exceptional handling of massive API specifications sets it apart from competitors. Our evaluation includes a real-world test with Twilio's OpenAPI document containing over 900 endpoints and 2000+ schema definitions. While other platforms "struggled or crashed with this spec, Bump loaded it in under three seconds and maintained smooth scrolling and instant search results." While not relevant to most companies,this performance advantage becomes valuable as APIs grow in complexity, offering a solution that doesn't degrade with scale. #### Ease of Use Setup is simple, with a streamlined process that jumps straight to API documentation without templates or complex configuration. OpenAPI parsing is very thorough and accurate, we didn't encounter any unexpected issues. From a styling perspective, it doesn't take you as close to "production-ready" as some of the other platforms, but it also doesn't require extensive customization or configuration before you see output. #### Theming & customization Some would argue that clarity is better than polish when it comes to docs. And that's true, but why not have both? Compared with Mintlify & Scalar, docs made on Bump are sinply not as "sexy". You can do the basics like: logo & brand colors, custom CSS, navbar. But docs tend to retain a very Bump-like feel to them. Appearance is a nice to have compared with performance, which is why Bump is still the best choice for companies with huge API surface areas. #### Developer experience Bump lacks some of the slick polish that Mintlify and Scalar offer. But the core functionality is undoubtably solid. And one place where Bump outperforms is when it comes to versioning. Bump allows you to create multiple versions of your API documentation, making it easy to manage changes and updates. Again, a feature that shines for enterprise's with gnarly API surface areas. #### Support and pricing Bump has a slightly steep payment model: - **Basic: free:** - 1 user - **Standard: $265/month** - For small teams - **Enterprise: custom pricing** - SSO, custom branding, custom domain, custom integrations --- ### ReadMe: Simple for Non-Technical Teams, Limited for Developers
![ReadMe docs with generated SDK code samples](/assets/blog/choosing-a-docs-vendor/readme/readme-custom-code-sample.png)
#### One special feature: One tool for the whole team ReadMe's standout capability being accessible to team members without technical expertise. While other platforms assume users have familiarity with Git, local development environments, or code-based workflows, ReadMe provides a completely web-based experience with visual editing tools that feel familiar to anyone comfortable with basic web interfaces. This empowers non-technical marketing teams, technical writers, or product managers to take ownership of documentation without depending on engineering resources, which can be valuable in organizations where resources are constrained. However, this feature highlights the fundamental trade-off that ReadMe makes: simplicity and accessibility for non-technical users at the expense of the automation, version control, and workflow integration that developers expect in modern documentation tools. The platform clearly prioritizes lowering the barrier to entry over providing the robust technical features that development teams need for sustainable documentation practices. #### Theming and customization ReadMe has been around for a while, so maybe it's not surprising that some of its default themes don't have the sparkle when compared to newer offerings like Scalar and Mintify. But that's largely a result of being a victim of their own success in the docs arena. The pro is that users will be intimately familiar with a Readme docsite, the con is that it's harder to feel unique. You can add [custom CSS and JavaScript](https://docs.readme.com/main/docs/custom-css-and-javascript) using ReadMe's online editor. This doesn't provide the same level of control as editing the source files directly, but it's a quick way to make small tweaks. #### Developer experience As mentioned, ReadMe's most significant weakness is for technical teams. We couldn't find a way to programmatically update the API reference when our OpenAPI document changes. Instead, updated documents need to be manually uploaded each time. This creates a disconnection between code and documentation that introduces multiple risks: - Documentation easily becomes outdated as APIs evolve - No integration with continuous integration/deployment pipelines - Manual processes introduce opportunities for human error - Potential bottlenecks when documentation updates depend on specific team members with access For non-technical teams maintaining stable APIs with infrequent changes, this limitation might be acceptable. For development teams working in agile environments with frequent iterations, this manual approach creates substantial maintenance overhead and reliability concerns. #### Support and pricing Readme has plans for companies of different sizes: - **Free:** Create a foundation using your API reference. - **Startup: $99/month** - For small teams who want to build out their docs and create a community. - **Business: $399/month** - For multi-role teams who want advanced styling and basic management tools. - **Enterprise: custom pricing** - For organizations looking to build scalable docs with multi-project management and security needs. --- ### Redocly: Tried & tested
![API Reference](/assets/blog/choosing-a-docs-vendor/redoc/api-reference.png)
### One special feature: ecosystem Redoc's standout advantage is its deep integration across the broader API development ecosystem. Unlike newer platforms that require specific adaptations or plugins, Redoc has become something of a standard that many frameworks support natively. Tools like FastAPI, Django, ASP.NET Core, and others often include built-in Redoc support, allowing teams to generate documentation with minimal additional configuration. This widespread adoption means teams can often implement Redoc without disrupting existing workflows or adding new dependencies. #### Ease of Use Setting up Redoc feels like returning to a tool you've used before — recognizable but showing its age. The IDE-style dashboard provides an environment that developers will find instantly familiar, with its project structure sidebar, editor pane, and preview panel arrangement. However, this familiarity comes with a dated user experience that lacks the polish of newer competitors. #### Theming & customization The familiarity and ecosystem integrations come at a cost. The documentation produced by Redocly tends to have a utilitarian aesthetic that lacks the visual appeal of newer platforms like Mintlify. The interface feels somewhat dated compared to modern web applications, with a functional but not particularly inspiring presentation. For teams prioritizing developer experience and modern design aesthetics, this represents a trade-off. You can customize nearly everything: typography, spacing, layout, color schemes, code blocks, but you're starting from behind relative to the look and feel of newer platforms. So you'll need to put in more effort to achieve a modern polished look. #### Developer experience Redoc's integration with development workflows represents its most enduring strength. The platform offers several approaches to keeping documentation synchronized with code: - Git integration for automatic rebuilds - API sync for periodic updates - CLI workflow for local development #### Support and pricing Redocly offers three subscription tiers: - **Pro: $10/user/month** 1 project - **Enterprise: $24/user/month** - Advanced security and support - **Enterprise+ (custom pricing)** - Premium support and advanced features --- ## Wrapping up Each of the five documentation vendors excels in different areas: - **Scalar** is a great all-rounder for developer-focused teams who want interactive documentation with powerful testing tools. - **Bump** optimizes performance for large APIs, making it ideal when reliability and speed are paramount. - **Mintlify** offers superior customization, making it perfect for projects where visual and functional control is key. - **ReadMe** remains a solid choice for teams that value a quick and easy setup over deeper automation and customization. - **ReDoc** is a popular choice for teams that just want a simple, functional documentation platform. Your choice of docs tool will depend on your team's priorities, but whichever platform you choose, Speakeasy's integration capabilities can enhance your API documentation, keeping it robust and developer-friendly. # choosing-an-sdk-vendor Source: https://speakeasy.com/blog/choosing-an-sdk-vendor import { Callout } from "@/mdx/components"; Without an SDK for your API, your users must handle raw HTTP requests themselves, leading to integration delays, avoidable bugs, and higher support costs. Even with good documentation, not having an SDK can slow adoption, frustrate developers, and increase churn. A well-designed SDK improves developer experience. It saves time, reduces integration errors, speeds up onboarding, and – importantly – can drive adoption and revenue. Today, you no longer need to build SDKs manually. Several tools generate SDKs from an OpenAPI document, including [Speakeasy](https://www.speakeasy.com/), [Fern](https://buildwithfern.com/), [Stainless](https://www.stainless.com/), and [OpenAPI Typescript Codegen](https://github.com/ferdikoomen/openapi-typescript-codegen). But with so many options available, how do you know which will save you the most time? Which generator will create SDKs that are easy to use, extend, and won't break when your API grows? Which will let you focus on building features instead of fixing generated code? In this article, we compare the main SDK generation options available today. We'll explain their strengths, weaknesses, and differences to help you decide which best suits your needs. ## What to look for in an SDK generator It's not enough to focus solely on the code produced when evaluating an SDK generator. You also need to consider factors impacting the SDK's developer experience (for internal and external devs) and how well the tool fits your workflow and budget: - **OpenAPI support:** Does the generator fully support the OpenAPI Specification, including handling schema validation and pagination? - **OAuth 2.0 support:** Does the generator handle OAuth client credentials properly, including token fetching and refreshing? - **Webhooks support:** Can the generator validate, parse, and process webhook events securely and reliably? - **Generated documentation and examples:** Does the generated SDK come with ready-to-use documentation and copy-paste examples for everyday API operations, making it easier for developers to get started quickly? - **SDK quality and structure:** Is the generated code readable, idiomatic, and easy to navigate? Does the generator produce ready-to-publish packages with minimal dependencies, or does it create bloated, messy code that your team would need to clean up? - **Streaming support:** Does the generator produce SDKs that properly handle large uploads and downloads using streams? - **SDK automation and CI/CD integration:** Can the generator integrate into your CI/CD pipeline and automatically regenerate SDKs as your API evolves? - **SDK customization options:** Does the tool let you customize naming, structure, and the SDK's behavior when necessary? - **Pricing:** Is the pricing transparent, scalable, and startup-friendly? Here's a quick comparison of how each SDK generator performs across these criteria. | **Feature** | **Speakeasy** | **Fern** | **Stainless** | **OpenAPI Typescript Codegen** | | ------------------------------------------- | --------------------------------------- | --------------------------------- | ------------------------------------------- | ------------------------------- | | **OpenAPI support** | ✅ Full support for OpenAPI 3.0 and 3.1 | ✅ Supports OpenAPI 3.1 | ⚠️ Limited; best results with custom format | ✅ Supports OpenAPI 2.0 and 3.0 | | **SDK quality (ready-to-publish packages)** | ✅ Yes | ✅ Yes | ✅ Yes | ⚠️ Basic structure | | **OAuth 2.0 / runtime validation** | ✅ Built-in schema validation | ✅ Built-in schema validation | ✅ Built-in schema validation | ❌ Not supported | | **Webhooks support** | ✅ Full support | ⚠️ Helper methods | ❌ Not supported | ❌ Not supported | | **Streaming support** | ✅ Uploads/downloads via streams | ⚠️ Limited (Fern Definition only) | ⚠️ Enterprise-only | ❌ Not supported | | **Documentation and examples** | ✅ Autogenerated docs and examples | ⚠️ Basic with OpenAPI | ⚠️ Basic; best with Stainless configuration | ❌ Minimal documentation | | **SDK automation and CI/CD integration** | ✅ GitHub Action / CI-ready | ✅ CLI / GitHub Action | ⚠️ Manual CLI | ✅ Easy CLI integration | | **SDK customization options** | ✅ High (OpenAPI extensions, overrides) | ⚠️ High, but via Fern Definition | ⚠️ Requires custom format | ⚠️ Limited (template edits) | | **Pagination handling** | ✅ Automatic support | ✅ Auto-pagination | ⚠️ Manual configuration needed | ❌ Manual | | **Retries built-in** | ✅ Yes | ✅ Yes | ✅ Yes | ❌ Manual | | **Pricing** | Free tier; paid plans from $250/mo | Paid plans from $250/mo | Free tier; paid plans from $250/mo | Free (open source) | Not sure which tool to pick? Here's how to choose based on your priorities: - Choose **Speakeasy** if you want the most complete feature set with the least manual work and you're OpenAPI-first. - Choose **Fern** if you need maximum control over SDK structure and don't mind a more complex setup. - Choose **OpenAPI Typescript Codegen** if you need a fast, open-source solution for basic TypeScript SDKs. ## Speakeasy Speakeasy's approach to SDK generation focuses on real-world API usage, offering advanced features and minimizing developer friction. Because Speakeasy uses OpenAPI documents to generate SDKs, it's a compelling choice for API-first teams. ### OpenAPI support Speakeasy fully supports OpenAPI versions 3.0 and 3.1. As Speakeasy relies on your [OpenAPI documents](/openapi) to generate SDKs, the quality of those documents directly impacts the results. To help ensure that OpenAPI documents are clean, accurate, and complete, Speakeasy validates them before generation, catching issues like missing required fields in requests or server responses that don't match the documented schema. For example, when sending a payload, if your OpenAPI document marks a field as required, the SDK will enforce it at the client level: ```js await sdk.createUser({ email: "user@example.com", // Required name: "John Doe", // Optional }); ``` If the email field is missing, the SDK throws a validation error without making a request. Speakeasy also handles [pagination](/openapi/paths/operations/responses#documenting-pagination) patterns natively. Whether your API uses offset, cursor, or custom pagination logic, Speakeasy can generate helper functions that abstract pagination away from the user. For example: ```js for await (const user of sdk.listUsersPaginated({ pageSize: 50 })) { console.log(user.name); } ``` No need to manually manage cursors, tokens, or page offsets. ### SDK quality and structure Speakeasy-generated SDKs are production-ready by default – idiomatic for each target language, aligned with community best practices, and free of bloated, deeply nested, or unreadable structures. Each SDK is designed for immediate publishing to registries like npm, PyPI, and Maven Central, with sensible package structure, typed clients, and minimal external dependencies. ### OAuth 2.0 support Speakeasy supports [OAuth 2.0](/openapi/security/security-schemes/security-oauth2) out of the box. If your OpenAPI document defines an OAuth 2.0 security scheme, Speakeasy automatically detects it and generates SDK code that manages tokens on behalf of your users, including requesting tokens, handling expirations, and refreshing them when needed. #### Configuring OAuth in your OpenAPI document Let's take a look at how to define an OAuth 2.0 client credentials flow in an OpenAPI document: ```yaml components: securitySchemes: oauth: type: oauth2 flows: clientCredentials: tokenUrl: https://api.example.com/oauth2/token scopes: read: Grants read access write: Grants write access security: - oauth: - read ``` Once this security scheme is defined in an OpenAPI document, Speakeasy-generated SDKs will prompt SDK users to provide a `clientID` and `clientSecret` when initializing the client: ```js const sdk = new SDK({ security: { clientID: "", clientSecret: "", }, }); ``` Behind the scenes, the SDK will: - Automatically fetch an access token from the token URL. - Retry requests with a fresh token if the current one has expired. - Add the `Bearer ` Authorization header for authenticated requests. No additional code is needed to manage token lifetimes for this flow. #### Customizing token retrieval For more advanced scenarios, such as integrating with nonstandard token endpoints or using custom headers during token exchange, you can write your token retrieval logic and pass it to the SDK: ```js import { withAuthorization } from "./oauth"; const sdk = new SDK({ security: withAuthorization("clientID", "clientSecret"), }); ``` This allows complete control over how tokens are requested, validated, cached, and refreshed. To support flows like resource owner password credentials, authorization code, or custom schemes, you must update your OpenAPI document accordingly and use Speakeasy [SDK hooks](/docs/customize/code/sdk-hooks) for additional logic. ### Webhooks support Speakeasy offers automatic [webhook](/openapi/webhooks) event validation and parsing. For example, it can generate functions that automatically verify webhook signatures and parse the payload correctly according to the OpenAPI event schema: ```js const event = sdk.verifyWebhookEvent(payload, signature); ``` This automatic verification and parsing ensures security and reduces the chances of mishandling or misparsing critical webhook notifications. ### Streaming support Speakeasy supports [streaming](/docs/customize/runtime/streaming) for download and upload endpoints – essential when handling large files or long-lived server responses. Instead of buffering the whole file in memory, risking performance issues or crashes, SDKs expose the response as a stream. To enable this behavior, define the endpoint in your OpenAPI document using the `application/octet-stream` content type with a binary format: ```yaml /streamable: get: operationId: streamable responses: "200": description: OK content: application/octet-stream: schema: type: string format: binary ``` This tells the SDK generator to treat the response as a raw byte stream. In a TypeScript SDK, the download logic becomes clean and efficient: ```js import fs from "node:fs"; import { Writable } from "node:stream"; import { SDK } from "@speakeasy/super-sdk"; async function run() { const sdk = new SDK(); const result = await sdk.streamable("UR123"); const destination = Writable.toWeb(fs.createWriteStream("./report.csv")); await result.data.pipeTo(destination); } run(); ``` Here, the `sdk.streamable("UR123")` call fetches a `ReadableStream`. The Node.js `Writable.toWeb()` method converts a standard file stream to a format compatible with the browser-style Streams API. Then `pipeTo()` streams the API's `ReadableStream` directly into the writable file stream, avoiding full in-memory buffering. ### Generated documentation and examples Alongside SDKs, Speakeasy automatically generates documentation in `README.md`, `RUNTIMES.md`, `FUNCTIONS.md`, and `USAGE.md` files. The docs include concise usage examples (like creating a resource, retries, and authentication) based directly on your OpenAPI document. Developers get the SDK package and a browsable documentation site located in the `docs` directory, ready to deploy. ### SDK automation and CI/CD integration Speakeasy supports [fully automated SDK generation](/docs/speakeasy-reference/generation/ci-cd-pipeline). It can integrate directly in your CI/CD pipelines (for example, in GitHub Actions or GitLab CI/CD) so that when you update your OpenAPI document, SDKs are rebuilt and published automatically, without manual intervention. ### SDK customization options Speakeasy provides flexible configuration for naming, error handling, retry strategies, pagination settings, and more, either through OpenAPI extensions or project-specific overrides. You can fine-tune the SDK to match your API's particular design patterns or your engineering team's preferences. ### Pricing [Speakeasy](/pricing) isn't open source but offers a **hosted SDK management platform** with automated generation, publishing, and updates. The **free tier** includes one SDK in a single language with GitHub integration and access to the complete OpenAPI toolchain. Paid plans start at **$250/month** and unlock features like pagination support and multi-language output. Higher tiers offer webhook support, event streaming, test generation, and enterprise-grade service-level agreements (SLAs). ## Fern Fern is a modern SDK generator built around its domain-specific language, [Fern Definition](https://buildwithfern.com/learn/api-definition/fern/overview). While it supports OpenAPI, Fern works best when you adopt its format. Fern is positioned for teams that want to generate SDKs quickly without thinking about structure, naming, or layout. But choosing convenience over control comes with trade-offs: more generated files, tighter ecosystem lock-in, and reduced interoperability with standard tools. ### OpenAPI support [Fern](https://buildwithfern.com/learn/api-definition/introduction/what-is-an-api-definition) supports OpenAPI 3.0 and 3.1. However, when relying solely on OpenAPI, you'll likely hit limitations around pagination, OAuth, documentation, or advanced type handling. The Fern OpenAPI parser covers most standard use cases. However, it is sensitive to inconsistent patterns and may miss edge cases like unconventional pagination and loosely defined endpoints. If standard pagination fields like `next_cursor` or `offset` are clearly described in the Fern Definition, the parser can recognize them and generate SDK methods accordingly. Link pagination is not supported, and the [customization](https://buildwithfern.com/learn/sdks/deep-dives/auto-pagination) for pagination is limited, as you can only define the `offset` and `results` fields: ```yaml paths: /api/orders: x-fern-pagination: offset: $request.page_number results: $response.results ``` [Speakeasy](https://www.speakeasy.com/docs/customize/runtime/pagination#configuring-pagination) allows you to define the type of pagination, whether the pagination inputs are in the request parameters or body, and also the pagination outputs. You can define fields on the outputs such as `numPages`, `results`, and `nextCursor`, so Speakeasy knows how to handle auto-pagination. ```yaml paths: /api/orders: x-speakeasy-pagination: type: offsetLimit inputs: - name: page_number in: parameters type: page outputs: numPages: $.data.numPages results: $.data.resultArray ``` ### SDK quality and structure Fern SDKs tend to be deeply nested and verbose. The generated TypeScript code includes multiple layers of abstraction and utility wrappers, making it hard to follow what's being executed. Request logic is often hidden behind custom serializers, and the separation between core logic and API-specific behavior is unclear. Naming conventions are inconsistent, and common operations span too many files, making the SDKs harder to read, debug, or extend. See the [SDK structure section](/post/speakeasy-vs-fern#sdk-structure) of our Speakeasy vs Fern post for an overview of the Fern SDK structure. ### OAuth 2.0 support Fern can generate SDKs that support [bearer token authentication (OAuth 2.0 access tokens)](https://buildwithfern.com/learn/sdks/capabilities/oauth). The client setup usually expects you to pass the token at initialization, and it automatically attaches it to API calls. For example, a Fern TypeScript SDK configuration with OAuth 2.0 would look like this: ```js client = new Client({ clientId: "YOUR_CLIENT_ID", clientSecret: "YOUR_CLIENT_SECRET", }); ``` The Fern SDK supports authorization flows such as `client-credentials` and `authorization-code`, but doesn't support more advanced or legacy flows like `resource_owner_password`. Also, you can't implement custom auth flows with Fern because there's no extensibility mechanism. By contrast, Speakeasy provides full flexibility via [SDK hooks](https://www.speakeasy.com/docs/customize/code/sdk-hooks), letting you inject custom authentication or [refresh](https://www.speakeasy.com/docs/customize/authentication/oauth#custom-refresh-token-flow) logic, or even build support for entirely custom auth schemes when needed. ### Webhooks support Fern SDKs provide a [constructEvent helper](https://buildwithfern.com/learn/sdks/capabilities/webhook-signature-verification) to verify webhook signatures and parse incoming payloads into typed event objects. This improves security and reduces boilerplate for consumers handling webhooks. That said, Fern leaves much of the responsibility for webhook handling to the developer. You must manually configure secrets, map the correct headers, and implement any error handling, replay protection, or logging logic. ### Streaming support Fern supports binary uploads using the `bytes` type in the Fern Definition, allowing you to model raw binary payloads (like MP3 or image files) as the request body. Fern expects `type: file` for responses, which allows the SDK to treat the returned data as a streamable file. However, this streaming behavior is only available when using Fern Definition. If you rely on OpenAPI, you must manually handle both uploads and downloads, without stream helpers. ### Generated documentation and examples If you use Fern Definition, the generated SDKs include well-structured inline documentation and developer-friendly examples of API operations. When working directly from OpenAPI without Fern-specific metadata, the generated SDK documentation is more basic and lacks detailed examples. The SDKs still include method signatures and basic type hints, but they rely heavily on how much information is in your original OpenAPI document. ### SDK automation and CI/CD integration Fern provides [tools that automate SDK generation](https://buildwithfern.com/learn/sdks/guides/publish-a-public-facing-sdk) whenever your API specification changes. However, this automation is primarily designed for projects using Fern Definition. Pure OpenAPI projects may require additional configuration or custom scripts to achieve seamless CI/CD automation. ### SDK customization options If you adopt the Fern Definition, Fern is highly customizable. You can configure naming conventions, error handling structures, pagination strategies, custom SDK behavior, and even the generated documentation. However, this flexibility comes with vendor lock-in. In many cases, you'll have to manage two sources of truth: your OpenAPI document and the Fern Definition. Speakeasy, on the other hand, lets you use a single OpenAPI document as the source of truth. For more custom behavior, you can write SDK hooks, update the OpenAPI document directly, or use an [Arazzo definition](https://www.speakeasy.com/openapi/arazzo) for advanced use cases like defining workflows. Fern also allows [custom code augmentation](https://buildwithfern.com/learn/sdks/capabilities/custom-code), but only after the SDK has been generated. ### Pricing [Fern's starter plan](https://buildwithfern.com/pricing) begins at **$250/mo per SDK** for up to 50 endpoints. By contrast, Speakeasy's free plan includes up to 250 endpoints per SDK with full OpenAPI support, automated generation, and GitHub integration. You'll need the **$600/mo** Pro plan to access OAuth, pagination, and webhook support. Even then, features like token refresh or signature validation are DIY. Enterprise support adds SLAs and migrations, but doesn't close the feature gap. ## Stainless Stainless is an SDK generation platform built around a custom configuration format rather than native OpenAPI support. Stainless produces polished SDKs with a clean developer experience, but it requires additional setup and manual integration for many advanced features. ### OpenAPI support Stainless partially supports the [OpenAPI Specification](https://app.stainless.com/docs/reference/openapi-support), so the best results require migrating to its custom Stainless format. Using plain OpenAPI documents can lead to missing features, such as pagination metadata, authentication flows, and webhook definitions. Stainless does not automatically infer pagination behavior from OpenAPI descriptions. Developers need to annotate their specs manually to enable proper pagination handling. If your OpenAPI document is your stack's single source of truth, you'll run into limitations unless you migrate to the Stainless domain-specific language. ### SDK quality and structure Stainless prioritizes clean SDKs with minimal dependencies and idiomatic formatting. However, the [SDK layout](/post/speakeasy-vs-stainless#stainless) lacks structure: Models, operations, and helpers are often mixed without clear separation. Tracing what's actually happening under the hood can take time. Error handling is thin, so expect to wire up your retry logic, response decoding, and fallback behavior. ### OAuth 2.0 support Stainless-generated SDKs do not manage OAuth tokens for you. Instead, developers must fetch access tokens manually and inject them into the client's headers. ```js import axios from "axios"; import TechBooks from "stainless-book-sdk"; async function getToken() { const res = await axios.post("https://api.example.com/oauth/token", { grant_type: "client_credentials", client_id: "client-id", client_secret: "client-secret", }); return res.data.access_token; } const token = await getToken(); const client = new TechBooks({ headers: { Authorization: `Bearer ${token}`, }, }); ``` There's no built-in retry, refresh, or expiration handling — you're on your own for all lifecycle logic. ### Webhooks support Stainless doesn't support webhooks. If you define webhook events, you'll need to implement your own verification (for example, HMAC or RSA) and deserialization logic. There's no helper like `constructEvent()` or built-in type-safe handler — just raw payloads. ### Streaming support Streaming support is only available for Enterprise customers. ### Generated documentation and examples Stainless SDKs generated from OpenAPI documents have limited inline documentation. Migrating to the Stainless format improves documentation quality, but compared to other generators like Speakeasy or Fern, Stainless provides fewer ready-to-use examples. ### SDK automation and CI/CD integration Stainless supports [automation for OpenAPI updates](https://app.stainless.com/docs/guides/automate-updates) through a GitHub Action or polling mechanism. The recommended setup uses a prebuilt GitHub Action that pushes OpenAPI document changes directly from your repository to Stainless. You can also configure the platform to poll a persistent URL (like a raw GitHub link) hourly for changes. However, Stainless does not provide a full CI/CD pipeline for SDK generation and publication beyond uploading your OpenAPI document. You're still responsible for managing SDK builds, versioning, and publishing to registries like npm or PyPI. Compared to platforms like Speakeasy that offer end-to-end automation – from OpenAPI document to SDK release – Stainless requires more manual setup and maintenance. ### SDK customization options Customization is only available when using the Stainless format. Using a plain OpenAPI document limits your ability to customize SDK behavior. Teams that want fine-grained control over SDK structure must migrate to Stainless's configuration file format. ### Pricing Stainless offers a [free plan with one local SDK and up to 50 endpoints](https://www.stainless.com/pricing). The Scale-up plan starts at $250/month per SDK, with a limit of 50 endpoints per SDK. For larger teams, the Business plan is priced at $800/month per SDK and allows up to 200 endpoints per SDK. Enterprise pricing is custom and includes higher limits, SLAs, and premium support. ## OpenAPI Typescript Codegen OpenAPI Typescript Codegen is a lightweight, open-source CLI tool that generates TypeScript or JavaScript SDKs directly from OpenAPI documents. While the tool is minimalistic and simple to set up, it requires significant manual work to produce production-grade SDKs. ### OpenAPI support OpenAPI Typescript Codegen fully supports OpenAPI 2.0 and 3.0 but interprets OpenAPI documents very literally: If the document lacks examples, pagination metadata, or detailed schemas, the generated SDK will be incomplete or inaccurate. The tool provides no built-in understanding of pagination patterns (such as offset- or cursor-based approaches) or advanced specification features like polymorphism with `oneOf` and `anyOf`. The quality of the generated SDK depends heavily on the precision of the OpenAPI document provided. ### SDK quality and structure The SDK generated by OpenAPI Typescript Codegen is clean but extremely basic: A simple folder structure (`/models`, `/services`), low-level HTTP wrappers using Fetch or Axios, and minimal or no error handling for API failures. The SDK feels closer to an API client scaffold than a complete developer experience platform. ### OAuth 2.0 support OpenAPI Typescript Codegen does not offer built-in helpers for OAuth flows. The developer must manually handle authentication, and token management and refresh logic must be implemented outside the SDK. ### Webhooks support The generator does not natively support webhook validation, parsing, or handling. It focuses on REST API clients and does not extend to webhook server-side integrations. ### Streaming support Streaming is not natively supported. Large uploads and downloads must be manually handled by adjusting Fetch or Axios request configurations. ### Generated documentation and examples OpenAPI Typescript Codegen generates basic inline TypeScript typings and function signatures. It does not provide ready-to-use examples or rich developer documentation. Developers are expected to rely on TypeScript's IntelliSense to navigate the SDK. There are no tutorials or built-in guides alongside the generated code. ### SDK automation and CI/CD integration Since it is a simple CLI tool, [OpenAPI Typescript Codegen](https://github.com/ferdikoomen/openapi-typescript-codegen/wiki/Basic-usage) easily integrates into any CI/CD pipeline. A typical GitHub Action step might look like this: ```yaml - name: Generate SDK run: | npx openapi-typescript-codegen --input ./openapi.json --output ./sdk ``` Automation is straightforward but requires custom scripting for tasks like versioning, publishing, and changelog generation. ### SDK customization options OpenAPI Typescript Codegen has limited support for SDK customization. Developers can modify templates slightly, but advanced customization like changing naming conventions or response handling requires forking the tool or manually post-processing the output. ### Pricing OpenAPI Typescript Codegen is fully [open source](https://github.com/ferdikoomen/openapi-typescript-codegen/blob/main/LICENSE) and free to use. There is no official enterprise support channel or paid offering available. ## Final thoughts If you are building an API product, you probably already have an OpenAPI document available – or you should. When choosing an SDK generator, prioritize tools that offer complete, native support for OpenAPI without requiring custom formats, workarounds, or extensive manual adjustments. Ultimately, the best SDK generator is the one that fits your team's workflow, spec quality, and appetite for customization. If you are looking for a tool that fully supports OpenAPI, helps you generate high-quality SDKs with minimal manual work, and makes it easier to ship faster, it's worth checking out the [Speakeasy tools](/product/sdk-generation). # Sample data Source: https://speakeasy.com/blog/choosing-your-framework-python import { CardGrid } from "@/components/card-grid"; import { pythonFrameworksData } from "@/lib/data/blog/python-frameworks"; We're fortunate to live in a time when a wide selection of Python API frameworks is available to us. But an abundance of choice can also be overwhelming. Do you go for the latest, trending option or stick with the tried-and-tested framework that offers security and control? Whether you're a startup founder who needs to deliver an MVP in a few weeks while taking scale and performance into consideration, or part of a large organization running hundreds of microservices needing reliable and robust technologies, choosing the right API framework is a critical decision. The key is recognizing that every framework choice involves trade-offs, which shift based on your project's unique needs. Failing to account for this can lead to frustration down the road. In this post, we discuss the factors to consider when choosing a REST API framework and explores popular options, highlighting each framework's strengths and weaknesses. At the end of the article, we'll suggest a pragmatic approach you can take to make an informed decision. ## Factors to consider when choosing a Python API framework ### Iteration speed For startups or fast-moving teams, the pressure to ship an MVP or new features quickly can outweigh concerns about the project's long-term architecture. But this short-term focus can lead to technical debt, making it harder to scale or adapt the API later. To strike the right balance between speed and maintainability, it helps to understand when speed is essential and when it's worth investing time in a more robust foundation. The solution lies in using tools that offer the flexibility to write code quickly while setting aside some initial scalability or performance concerns, with the option to refactor and evolve your architecture as your project grows. Start with a simple, script-like setup for exposing endpoints without committing to a solid architecture upfront. Once the business is stable, you can take advantage of the framework's features to transition to a more complex and robust architecture. ### Enterprise needs: Scale and security Your MVP has succeeded, and your project now serves a significant user base. Or maybe you're operating in an enterprise environment, building a service that must handle thousands or even millions of daily requests. While flexibility is still appealing at this stage, relying on tools that prioritize flexibility over structure is no longer wise. Instead, focus on well-structured frameworks designed to help with scalability, simplify complex processes, and abstract away the challenges introduced by your growing needs. When choosing a framework for mature or large-scale projects, you need to consider: - **Request volume:** The number of requests your application needs to handle. - **Authorization:** How to manage user permissions securely and efficiently. - **Database optimization:** Ensuring database queries are performant and scalable. - **Logging:** Implementing proper logging for monitoring and debugging. - **Performance:** Maintaining responsiveness under heavy traffic and load. While lightweight frameworks can handle these challenges with careful implementation, your top priorities should shift to performance, robustness, and security. When evaluating frameworks for these needs, consider these three critical factors: - **Framework maturity and adoption:** A framework with wide industry adoption can be a sign of reliability. A strong community and long-standing development history often reflect a framework's stability and available support. - **Security:** A framework with many built-in features may introduce security vulnerabilities. Assess the framework's history of handling security issues, its track record with security updates, and the quality of its documentation. - **Robustness:** Evaluate the framework's architecture for its ability to abstract complex tasks effectively, ensuring scalability and maintainability over time. ### Async support Asynchronous programming is known for its performance benefits, especially in non-blocking operations. For example, imagine an API that handles file uploads: The user doesn't need the upload to finish immediately or receive a download link right away. They just want confirmation that the process has started and that they'll be notified of its success or failure later. This is where async frameworks shine, allowing the API to respond without waiting for the file upload to complete. Synchronous frameworks like Flask or Django can still handle asynchronous-like tasks using background job libraries like Celery paired with tools like Redis or RabbitMQ. While these frameworks have introduced partial async support in their architectures, they are not fully asynchronous yet. Background job solutions like Celery, Redis, and RabbitMQ are robust for task delegation, but they come with additional setup complexity and don't achieve proper non-blocking behavior within the API. Frameworks built with async programming in mind, like Tornado and FastAPI, provide a more intuitive coding experience for async tasks. ## Popular Python API frameworks ### Flask-RESTX: Familiar, lightweight, and flexible Flask alone is sufficient to build a REST API. However, to add important REST API features like automatic Swagger documentation, serialization, and error handling, [Flask-RESTX](https://flask-restx.readthedocs.io/) offers tools that simplify additional parts of your workflow. Here's an example that creates an application to list payments: ```python filename="app.py" from flask import Flask from flask_restx import Api, Resource, fields app = Flask(__name__) api = Api(app, doc="/docs") # Swagger UI documentation available at /docs ns = api.namespace('payments', description="Payment operations") payment_model = api.model('Payment', { 'id': fields.Integer(description="The unique ID of the payment", required=True), 'amount': fields.Float(description="The amount of the payment", required=True), 'currency': fields.String(description="The currency of the payment", required=True), 'status': fields.String(description="The status of the payment", required=True), }) payments = [ {'id': 1, 'amount': 100.0, 'currency': 'USD', 'status': 'Completed'}, {'id': 2, 'amount': 50.5, 'currency': 'EUR', 'status': 'Pending'}, {'id': 3, 'amount': 200.75, 'currency': 'GBP', 'status': 'Failed'}, ] @ns.route('/') class PaymentList(Resource): @ns.marshal_list_with(payment_model) def get(self): return payments api.add_namespace(ns) if __name__ == "__main__": app.run(debug=True) ``` This code snippet creates an application that runs on port **5000** and provides two endpoints: - `/payments`, for listing payments. - `/docs`, for automatically documenting the payments endpoint. The Flask-RESTX marshaling feature is noteworthy for how it automatically maps the results – whether from a database, file, or API request – to a defined schema and sends a structured response to the client. This functionality ensures consistency and reduces boilerplate code for formatting responses. The Flask ecosystem gives you the flexibility to create your application in the way that suits your needs. When the time comes to scale, Flask combined with [Flask-RESTX](https://flask-restx.readthedocs.io/) provides you with the features you need to handle larger, more complex projects effectively. ### Sanic: For lightweight and production-ready real-time APIs Sanic (not to be confused with Sonic the Hedgehog, though it's just as speedy) is a lightweight, asynchronous Python web framework designed for high-performance and real-time applications. While these characteristics might suggest complexity, writing an application that serves both an HTTP endpoint and a WebSocket server is surprisingly straightforward. ```python filename="app.py" from sanic import Sanic from sanic.response import json app = Sanic("ConfigAPI") configs = { "app_name": "My App", "version": "1.0.0", "debug": True, "max_connections": 100, "allowed_hosts": ["localhost", "127.0.0.1"], } @app.get("/configs") async def get_configs(request): return json(configs) if __name__ == "__main__": app.run(host="127.0.0.1", port=8000, debug=True) ``` Sanic intuitively handles static files, making it a user-friendly alternative to popular frameworks like [Django, which can require more complex configurations for similar tasks](https://www.quora.com/Why-is-Django-making-handling-static-files-so-difficult). ```python filename="app.py" app = Sanic("ConfigAPI") app.static('/static', './static') ``` Another point in Sanic's favor is its interesting approach to handling TLS, a process that can be complicated to understand and set up. With Sanic, you can start your server using your certificate files, or even better, let it automatically set up local TLS certificates, enabling secure access with little configuration. ```bash sanic path.to.server:app \--dev \--auto-tls ``` ### FastAPI: Build modern and highly typed REST APIs FastAPI's excellent developer experience has made it one of the most popular Python frameworks. By combining async programming, type hints, and automatic OpenAPI document generation, FastAPI enables you to create highly documented APIs with minimal effort. FastAPI's design is also async-first, making it an excellent choice for real-time APIs, high-concurrency workloads, and systems needing rapid prototyping with built-in tools. FastAPI offers modern convenience and a healthy ecosystem of complementary tooling without compromising on performance. The following code example demonstrates creating a REST API for listing and creating invoices. ```python filename="app.py" from fastapi import FastAPI, HTTPException from pydantic import BaseModel from typing import List app = FastAPI() class Invoice(BaseModel): id: int customer_uid: str amount: float status: str # In-memory storage for invoices invoices = [ Invoice(id=1, customer_uid="4r3dd", amount=250.50, status="Paid"), Invoice(id=2, customer_uid="f3f3f3f", amount=150.00, status="Pending"), ] @app.get("/invoices", response_model=List[Invoice]) async def list_invoices(): return invoices ``` ### Django REST framework If what you care about is security, reliability, and maturity, [Django REST framework (DRF)](https://www.django-rest-framework.org/) is what you want. Django is the most mature Python framework and rose to prominence thanks to its abstractions of the tedious but essential parts of backend development: authentication, authorization, logging, multiple database connections, caching, testing, and much more. However, this abstraction comes with trade-offs. Django is not especially flexible or lightweight, and its enforced Model-View-Template (MVT) structure can feel verbose and rigid compared to more modern frameworks. However, if you embrace its design principles, Django can be one of the most stable and effective frameworks you've ever used. When it comes to async support, DRF does not currently support async functionality. This limitation means you cannot create async API views or viewsets using DRF, as its core features – like serializers, authentication, permissions, and other utilities – are not designed to work asynchronously. Third-party package [ADRF (Async DRF)](https://github.com/em1208/adrf) adds async support, but it's not officially supported and may not be stable for production. That undermines the core value of Django REST framework: stability. To create an API with DRF, you need to define a model first. ```python filename="models.py" from django.db import models class Item(models.Model): name = models.CharField(max_length=255) description = models.TextField() price = models.DecimalField(max_digits=10, decimal_places=2) created_at = models.DateTimeField(auto_now_add=True) def __str__(self): return self.name ``` Then, you need to define a serializer that will convert the Python object retrieved from the Django ORM to a JSON object and vice versa. ```python filename="serializers.py" from rest_framework import serializers from .models import Item class ItemSerializer(serializers.ModelSerializer): class Meta: model = Item fields = ['id', 'name', 'description', 'price', 'created_at'] ``` Next, you need to write a view (or in standard terms, controller) to handle the API logic, in this case, listing. ```python filename="views.py" from rest_framework.generics import ListCreateAPIView from .models import Item from .serializers import ItemSerializer class ItemListCreateView(ListCreateAPIView): queryset = Item.objects.all() serializer_class = ItemSerializer ``` Finally, you need to register the view in a `urls.py` file. ```python from django.urls import path from .views import ItemListCreateView urlpatterns = [ path('items/', ItemListCreateView.as_view(), name='item-list-create'), ] ``` This example illustrates how verbose Django can be. But by following its well-documented architecture, you ensure your application is robust and scalable while following proven design principles. ### Tornado: Pure async logic Tornado is a lightweight framework built entirely around asynchronous programming, making it ideal for building APIs where non-blocking I/O is critical, like WebSocket-based applications or systems with high-concurrency needs. If you don't have the immediate pressure of needing an extensive feature set or an existing ecosystem, Tornado can be an excellent choice for applications requiring pure async workflows. ```python filename="app.py" from tornado.ioloop import IOLoop from tornado.web import Application, RequestHandler import json # In-memory storage orders = [] # Handler to list all orders class OrderListHandler(RequestHandler): async def get(self): self.set_header("Content-Type", "application/json") self.write(json.dumps(orders)) # Initialize the Tornado app def make_app(): return Application([ (r"/orders", OrderListHandler), # Endpoint to list all orders ]) if __name__ == "__main__": app = make_app() app.listen(8888) print("Server is running on http://127.0.0.1:8888") IOLoop.current().start() ``` However, Tornado lacks some of the built-in tools and abstractions found in more modern frameworks like FastAPI, meaning you might spend more time building features available out of the box elsewhere. ## Making pragmatic choices The Python API frameworks we've discussed each have distinct strengths and trade-offs, but choosing the right framework for your project might still be a daunting task. To help you select a framework, we've created a flowchart that simplifies the decision-making process and a table that maps use cases to recommended frameworks. To use these resources, start with the flowchart to narrow your options based on your project's stage, requirements, and priorities. Then, consult the table to match your use case and requirements to recommended frameworks. ![A flowchart for choosing a Python framework](/assets/blog/choosing-your-framework-python/framework-decision-diagram.png) | **Use case** | **Requirements** | **Recommended frameworks** | |-----------------------------|---------------------------------------------|--------------------------------------| | **MVP with limited resources** | Quick setup, simplicity, flexibility | Flask-RESTX, FastAPI | | **Complex project** | Scalability, structure, robust tools | Django + DRF | | **Secure enterprise application** | Strong security, maintainability, scalability | Django + DRF | | **Fully async workload** | High concurrency, non-blocking performance | FastAPI, Tornado | | **Real-time application** | WebSocket support, low latency | Tornado, Sanic | | **Existing project** | Gradual migration to async or scaling needs | Django (with ASGI), FastAPI | Consider: 1. What does your project need most — stability or speed? 2. Are you starting fresh or scaling an existing application? 3. Does the framework support your required features without adding unnecessary risk? 4. How well does the framework align with your team's expertise? If your team has extensive experience with one framework, that might be your go-to for creating a REST API. If stability, reliability, and enterprise-grade features are your priorities, then [Django REST framework (DRF)](https://www.django-rest-framework.org/) probably makes sense. If your priorities are a modern developer experience, performance, or emerging async capabilities, then a cutting-edge framework like **FastAPI** is a great choice. # contract-testing-with-openapi Source: https://speakeasy.com/blog/contract-testing-with-openapi import Image from "next/image"; import { Table } from "@/mdx/components"; We've all heard that infernal phrase, "It works on my machine." Scaling any solution to work across many machines can be a challenge. The world of APIs with disparate consumers is no different. What if there was a well-defined contract between them? But APIs and consumers change over time, which begs the question: How do we ensure our systems stick to their agreements? The answer is contract testing. In this article, we'll explore contract testing, how it fits into the testing pyramid, and why it's important. We'll also walk through how to implement contract testing using OpenAPI, a popular API specification format. Finally, we'll discuss how you can generate contract tests automatically using OpenAPI and Speakeasy. ## What is contract testing? Contract testing verifies that two parties who interact via an API stick to the contract they agreed upon. We'll call these parties a consumer and a provider, based on the direction of the API calls between them. The consumer is the system that makes API calls, and the provider is the system that receives and responds to the API call. In most cases, the contract between our parties is defined by an API specification, such as OpenAPI. Even if there is no API specification, there is always, at the very least, an implicit agreement between the consumer and the provider. The consumer expects the provider to respond in a certain way, and the provider expects the consumer to send requests in a certain way. This explicit or implicit agreement is the contract. Contract testing, from the consumer's perspective, is about verifying that the provider sticks to the contract. From the provider's perspective, it's about verifying that the consumer sticks to the contract. ## How contract testing differs from unit testing, integration testing, and end-to-end testing Testing strategy is often represented as a pyramid, with unit testing at the base, followed by other testing methodologies in ascending order of complexity and scope. The idea is that the majority of tests should be at the base of the pyramid, with fewer tests at each subsequent level. The testing pyramid is a useful model for thinking about how different types of tests fit together in a testing strategy. It helps to ensure that the right types of tests are used in the appropriate proportions, balancing the need for comprehensive testing with the need for fast feedback.
Let's discuss each level of the pyramid in more detail, starting from the base. ### Unit testing: Does this function work as expected? Unit testing forms the base of the testing pyramid. These tests focus on individual components or functions in isolation, typically mocking any dependencies. They are fast to run and easy to maintain, but don't test how components work together. In the consumer's context, a unit test might verify whether the function that deserializes a JSON object into a Python object works correctly, without making any external API calls. Unit tests are essential for catching bugs early in the development process and providing fast feedback to developers. They are also useful for ensuring that code behaves as expected when refactoring or adding new features. However, they don't provide much confidence that the system as a whole works correctly. ### Contract testing: Do we honor the API specification? Moving up the pyramid, we have contract tests. Contract testing sits between unit testing and integration testing. It focuses specifically on the interactions between the provider and consumer for a given call, ensuring that the API contracts are honored. Contract tests are more complex than unit tests but less complex than integration tests. A contract test verifies that a consumer can create requests with specific data and correctly handle the provider's expected responses or errors. This might be accomplished with mocked request or response data based on the contract. For example, an API contract test for an order creation endpoint might verify that request data correctly maps integer item IDs to quantities and that the response decodes to an expected success with an integer order ID. Contract tests are useful for catching issues that arise when the consumer or provider strays from the agreed-upon contract. They provide a level of confidence that the system works as expected when the consumer and provider interact. They are also useful for catching breaking changes early in the development process. ### Integration testing: Do systems work together? Further up, we find integration tests. These verify that different components of a system work together correctly. They are more complex than unit tests and contract tests, and may involve multiple components or services. An integration test might verify that the consumer can successfully make an API call to the provider and receive a valid response. This test would involve both the consumer and provider, and would typically run in a test environment that mirrors the production environment. Because integration tests involve multiple systems, they are useful for catching issues that arise when components interact, such as network issues between two services. ### End-to-end testing: Does the user flow work as expected? At the top of the pyramid are end-to-end tests. These test the entire user flow and supporting systems from start to finish. They provide the highest level of confidence but are also the slowest to run and most difficult to maintain. In our API context, an end-to-end test might involve making a series of API calls that represent a complete user journey, verifying that the system behaves correctly at each step. This could include creating a resource, updating it, retrieving it, and finally deleting it, all through the API. When end-to-end tests fail, it can be challenging to identify the root cause of the failure, as the problem could be in any part of the system. Due to their complexity and cost, they are often used as a final check before deploying to production, rather than as part of the regular development process. ### Testing pyramid summary Here's a summary of the different types of tests and how they compare:
## Why contract testing is important Over time, APIs change in response to changing requirements, sometimes in subtle and imperceptible ways. For example, a provider may change the format of a field in a certain response from a string to an integer. This change may seem innocuous to the provider but could have catastrophic effects for the consumer. Contract testing mitigates this risk by ensuring that any changes to the API contract are detected early in the development process. When the provider updates the API, corresponding contract tests will fail if the update is not backward compatible. This failure acts as an immediate signal that the change needs to be reviewed, preventing breaking changes from reaching production. Consider this example: A subscription management platform (the provider) has an endpoint `/plan/{id}` that returns a subscription plan based on the plan ID. The consumer expects the response to include an `amount` field, which is an integer representing the cost of the plan. If the provider changes the `amount` field from an integer to a string, the consumer's contract test will fail, alerting the consumer to the breaking change. In this example, a contract test would catch the breaking change early in the development process, before it reaches production. The consumer and provider can then work together to resolve the issue, ensuring that the API contract is honored. ## How to implement contract testing with OpenAPI Let's walk through the process of implementing contract testing using [OpenAPI](/openapi/), focusing on both the consumer and provider perspectives. ### Step 1: Create a new project We'll start by creating a new project for our consumer and provider code. We'll use a simple subscription management API as an example. In the terminal, run: ```bash mkdir contract-example cd contract-example ``` Let's create a new TypeScript project for our SDK and tests: In the terminal, run: ```bash npm install --save-dev typescript npx tsc --init ``` Select the default options when prompted. ### Step 2: Define the OpenAPI specification We'll start by writing an OpenAPI specification for our API. In most cases, the provider will define the OpenAPI specification, as they are responsible for the implementation of the API. Here's a basic example of an OpenAPI specification. Save it as `subscriptions.yaml` in the root of your project. The document contains a `/plan/{id}` endpoint, which has a single `GET` operation that retrieves a subscription plan by ID. It expects an integer `id` parameter in the path. We'll focus on the `200` response for now. The response should be a JSON object with the subscription plan details, as defined in the `Subscription` schema. The schema includes fields for `id`, `name`, `amount`, and `currency`. ```yaml filename="subscriptions.yaml" openapi: 3.1.0 info: title: Subscription Management API version: 1.0.0 servers: - url: http://127.0.0.1:4010 description: Local server tags: - name: subscription description: Subscription management security: - api_key: [] paths: /plan/{id}: get: operationId: getPlanById tags: - subscription summary: Get a subscription plan by ID parameters: - name: id in: path required: true schema: type: integer examples: basic: value: 1 premium: value: 2 responses: "200": description: Successful response content: application/json: schema: $ref: "#/components/schemas/Subscription" examples: basic: value: id: 1 name: "Basic" amount: 100 currency: "USD" premium: value: id: 2 name: "Premium" amount: 200 currency: "USD" components: schemas: Subscription: type: object properties: id: type: integer name: type: string amount: type: integer currency: type: string example: id: 1 name: "Basic" amount: 100 currency: "USD" securitySchemes: api_key: type: apiKey name: X-API-Key in: header ``` This example OpenAPI specification only defines the happy path for the `/plan/{id}` endpoint. In a real-world scenario, you would define additional paths, operations, and error responses to cover all possible scenarios. ### Step 3: Create an SDK with Speakeasy We'll use Speakeasy to create a TypeScript SDK from the OpenAPI specification. If you don't have Speakeasy installed, you can install it from the [Introduction to Speakeasy](/docs/introduction/introduction#getting-started) guide. With Speakeasy installed, run: ```bash speakeasy quickstart ``` When prompted, select the `subscriptions.yaml` file and choose TypeScript as the target language. We decided on `Billing` as the SDK name, and `billing` as the package name. ### Step 4: Add tests Now that we have an SDK, we can write tests to verify that the SDK handles the API responses correctly. Let's install the necessary dependencies: ```bash npm i --save-dev vitest ``` Create a new `tests` directory in the root of your project. Then, create a new file, `tests/subscription.test.ts`: ```typescript filename="tests/subscription.test.ts" import { expect, test } from "vitest"; import { Billing } from "../billing-typescript/src/index.ts"; test("Subscription Get Plan By Id Basic", async () => { const billing = new Billing({ apiKey: process.env["BILLING_API_KEY"] ?? "", }); const result = await billing.subscription.getPlanById({ id: 1, }); expect(result).toBeDefined(); expect(result).toEqual({ id: 1, name: "Basic", amount: 100, currency: "USD", }); }); ``` Add the following script to your `package.json`: ```json filename="package.json" { "scripts": { "test": "vitest run" } } ``` Now you can run the tests: ```bash npm run test ``` This should run the test and verify that the SDK correctly handles the API responses, but since we haven't started a server yet, the test will fail. ### Step 5: Start a mock server We'll use Prism to start a mock server that serves responses based on the OpenAPI specification. Add Prism as a dev dependency: ```bash npm install --save-dev @stoplight/prism-cli ``` Then, add a new script to your `package.json`: ```json filename="package.json" { "scripts": { "test": "vitest run", "mock": "prism mock subscriptions.yaml" } } ``` In a new terminal window, run: ```bash npm run mock ``` This will start a mock server at `http://127.0.0.1:4010`. ### Step 6: Run the tests Now that the mock server is running, you can run the tests again: ```bash npm run test ``` This time, Prism returns a `401` status code because we haven't provided an API key. Let's run the test with the `BILLING_API_KEY` set to `test`: ```bash export BILLING_API_KEY=test npm run test ``` ```txt $ vitest run RUN v2.1.1 /Users/speakeasy/contract-example ✓ tests/subscription.test.ts (1) ✓ Subscription Get Plan By Id Basic Test Files 1 passed (1) Tests 1 passed (1) Start at 07:28:38 Duration 630ms (transform 191ms, setup 0ms, collect 199ms, tests 150ms, environment 0ms, prepare 86ms) ``` The test should now pass, verifying that the SDK correctly handles the API response. ### Step 7: Test for correctness We've validated that the SDK can correctly handle an API response by interacting with a mock server, but we haven't confirmed whether the response conforms to the contract. To make this a true contract test, let's verify that both the consumer and provider behaviors align with the agreed-upon OpenAPI specification. We'll add a contract-validation step to the test, then use Ajv, a JSON Schema validator, to validate the response against the OpenAPI schema. ```bash npm install --save-dev ajv ajv-errors ajv-formats yaml ``` Create a new file, `validateSchema.ts`: ```typescript filename="validateSchema.ts" import { readFileSync } from "fs"; import Ajv from "ajv"; import addErrors from "ajv-errors"; import addFormats from "ajv-formats"; import yaml from "yaml"; // Load and parse the OpenAPI specification const openApiSpec = yaml.parse( readFileSync("./subscriptions.yaml", "utf8"), ) as any; // Initialize Ajv with formats and error messages const ajv = new Ajv({ allErrors: true, strict: false }); addFormats(ajv); addErrors(ajv); // Compile the schema for the Subscription response const subscriptionSchema = { ...openApiSpec.components.schemas.Subscription, }; const validate = ajv.compile(subscriptionSchema); export const validateSubscription = (data: any) => { const isValid = validate(data); if (!isValid) { console.error(validate.errors); throw new Error("Validation failed"); } }; ``` Update the test to include the contract-validation step: ```typescript filename="tests/subscription.test.ts" mark=3,19 import { expect, expectTypeOf, test } from "vitest"; import { Billing } from "../billing-typescript/src/index.ts"; import { validateSubscription } from "../validateSchema.ts"; test("Subscription Get Plan By Id Basic", async () => { const billing = new Billing({ apiKey: process.env["BILLING_API_KEY"] ?? "", }); const result = await billing.subscription.getPlanById({ id: 1, }); expect(result).toBeDefined(); expect(result).toEqual({ id: 1, name: "Basic", amount: 100, currency: "USD", }); validateSubscription(result); // Contract validation }); ``` Now when you run the tests, the contract validation will ensure that the response from the mock server matches the OpenAPI specification. ```bash npm run test ``` ## Generating contract tests automatically with OpenAPI Manually writing contract tests can be a time-consuming and error-prone process. If you're starting with an OpenAPI document as your contract, you may be able to automatically generate tests that conform to your contract. By generating contract tests, you reduce the risk of human error, save significant development time, and ensure that tests are always kept up to date. The biggest advantage of automated test generation is the assurance that your tests are based on the API specification. This means that all aspects of the API contract, from endpoint paths and methods to data types and constraints, are accurately represented in the generated tests. A drawback of basing tests on OpenAPI documents is that the OpenAPI Specification does not currently have built-in support for test generation. Although examples of requests and responses allow test case generation, there are still challenges in linking request and response pairs to each other. These are problems we're working hard to overcome at Speakeasy. ## Speakeasy test generation At Speakeasy, we enable developers to automatically test their APIs and SDKs by creating comprehensive test suites. Shipping automated tests as part of your SDKs will enable your team to make sure that the interfaces your users prefer, your SDKs, are always compatible with your API. We ensure your APIs and SDKs stick to the contract so that you can focus on shipping features and evolving your API with confidence. The process of adding tests with Speakeasy is straightforward: Add detailed examples to your OpenAPI document, or describe tests in a simple and well-documented YAML specification that lives in your SDK project. Speakeasy will regenerate your tests when they need to change, and you can run the tests as part of development or CI/CD workflows. ## Speakeasy test generation roadmap Looking ahead, Speakeasy's testing roadmap includes broader language support, advanced server mocking, ability to run contract tests on past versions of the API and SDK, and using the Arazzo specification to string together multiple contract tests. With these features, you'll be able to monitor the health of all your SDKs and APIs in one place. We're also working on support for behavior-driven development (BDD) and end-to-end (E2E) testing by embracing OpenAPI and the recently published Arazzo specification for complex testing workflows. # Connect your agent to your API (adapt to your setup) Source: https://speakeasy.com/blog/cost-aware-pass-rate AI benchmarks have a giant blind spot: they celebrate agents that succeed at any cost. Admiring an agent's position on today's leaderboards is like praising a brute force search algorithm for its 100% success rate. If accuracy is the only metric, brute force is perfectly acceptable. Bigger AI models win by throwing more parameters and reasoning cycles at problems, because this is what the benchmarks reward. Even a stopped clock is right twice a day, but that doesn't make it a good timepiece. New research from Bloomberg shows that by taking cost into account while optimizing tool descriptions and agent instructions, we can achieve similar or even better performance at a fraction of the cost. The paper, [A Joint Optimization Framework for Enhancing Efficiency of Tool Utilization in LLM Agents](https://web.archive.org/web/20250818044410/https://aclanthology.org/2025.findings-acl.1149/), introduces a new measure for how well language models use tools: Cost-Aware Pass Rate (CAPR). Instead of merely asking, "Did the model use the tool?" the study adds, "and at what cost?" Anyone building agents or tools for agents could benefit from this research. We'll summarize the key finding here and look at a practical example of how to implement the joint optimization framework for your own APIs and agents. ## The research: Context vs inference scaling The Bloomberg team tested their joint optimization framework across 16,464 APIs in StableToolBench, and against real-world scenarios in RestBench. They compared two fundamental approaches: **Context optimization** (improving instructions and tool descriptions) versus **inference scaling** (adding more reasoning steps, like Chain-of-Thought[^1] or tree search[^2]). [^1]: **Chain-of-Thought (CoT)** encourages models to generate intermediate reasoning steps before arriving at a final answer. Rather than jumping directly to conclusions, CoT prompts guide models to "think out loud" through problems step by step, significantly improving performance on complex arithmetic, commonsense, and symbolic reasoning tasks. [^2]: **Tree search methods** like depth-first search (DFS) extend linear reasoning approaches by exploring multiple solution paths simultaneously. When applied to LLM tool use, these methods maintain a backup mechanism to quickly restart from the last successful state when a tool call fails, theoretically improving both effectiveness and efficiency compared to simple retry strategies. The research suggests that context optimization leads to better performance and lower costs compared to inference scaling alone. When agents only have access to vague tool descriptions, they often resort to trial-and-error approaches, stubbornly repeating slightly varied queries until they stumble on a successful one. Sophisticated reasoning algorithms can't compensate for poor documentation. ## Is this just prompt engineering? Not quite. While improving prompts is an important part of the solution, tool descriptions may play a bigger role. Perhaps the most surprising finding from this research is that only improving tool descriptions results in a bigger gain than only doing instruction tuning. While **joint optimization** (improving both tool descriptions and agent instructions) leads to the best outcomes, good tool descriptions can actually be more important than good prompts. ## Why context quality matters more than we thought LLMs select tools and make tool calls based entirely on the available context. Tools with incomplete or vague descriptions cause agents to iterate through multiple attempts before finding and using the right tool effectively. This is compounded by incomplete instructions or guidance on how to use the tools. When tools depend on each other, failures can cascade, leading to a situation where the agent's inability to effectively use one tool impacts its interactions with others. Improving a single tool's description can lead to better performance across a suite of interdependent tools. Giving an agent more reasoning capabilities, like the ability to perform Chain-of-Thought reasoning about which tools to pick and how to use them, may reduce the impact of these issues. However, inference scaling doesn't make up for missing context. The research data shows that context optimization provides 10-30% cost reduction at fixed accuracy, while inference scaling increases costs by 200-500% for marginal accuracy gains. ## Practical implementation: The joint optimization framework Here's how to implement the Bloomberg research team's joint optimization approach for your own APIs and agents. The process works with any tool-calling system, whether it's MCP servers, function calling, or custom APIs. ### The challenge: Generic tool descriptions Most API documentation follows patterns like these: - `listItems`: "List all items" - `createItem`: "Create a new item" - `updateItem`: "Update an item" - `deleteItem`: "Delete an item" While technically accurate, these descriptions don't help LLMs choose between similar operations, understand required parameters, or handle edge cases effectively. ### Step 1: Establish baseline measurement First, connect your agent to your existing API and collect interaction data. The key is measuring both success and efficiency: ```python agent = YourAgent( system_prompt="Your current system prompt", tools=your_existing_tools ) # Test with realistic user queries test_queries = [ "Create a new task called 'Review quarterly reports'", "Show me all my current tasks", "Update the first task to mark it as high priority", "Delete any completed tasks" ] # Collect performance data for query in test_queries: interaction = await agent.process_query(query) # Log: query, tools called, success rate, response time ``` What to measure: - **Success rate:** Did the agent complete the task correctly? - **Tool call efficiency:** How many API calls were needed? - **Response time:** How long did each interaction take? - **Error patterns:** Which operations consistently fail? ### Step 2: Calculate the Cost-Aware Pass Rate (CAPR) Unlike traditional success metrics, CAPR penalizes inefficient tool usage: ```python def calculate_capr(interactions, efficiency_threshold=5): scores = [] for interaction in interactions: success = interaction.success tool_calls = len(interaction.tool_calls) if tool_calls > efficiency_threshold: scores.append(0) # Too inefficient else: efficiency = 1 - (tool_calls / efficiency_threshold) scores.append(success * efficiency) return sum(scores) / len(scores) ``` ### Step 3: Apply joint optimization The Bloomberg framework optimizes both system prompts and tool descriptions together: ```python # Analyze current performance patterns analysis = analyze_interactions(interactions) # Generate coordinated improvements optimized_prompt, optimized_tools = joint_optimization( interactions=interactions, current_prompt=your_system_prompt, current_tools=your_tool_descriptions ) ``` ### Step 4: Update your tool documentation Apply the optimized descriptions to your tool documentation, depending on how your tools are defined. For example, if your tools are part of a [Gram-hosted MCP server](/docs/gram/introduction), update tool descriptions in the Gram admin interface or directly in the OpenAPI documents. ### Step 5: Validate improvements Test your optimized agent with the same queries to measure improvements. ## Example: TODO MCP server hosted by Gram As an example, we tested the joint optimization framework on a TODO MCP server hosted by Gram. Here's the step-by-step process we followed, including the results we obtained. ### Step 1: Initial server setup We started with a basic TODO API hosted on Gram with these generic tool descriptions: - `listTodos`: "List all todos" - `createTodo`: "Create a new todo" - `updateTodo`: "Update a todo" - `deleteTodo`: "Delete a todo" The OpenAPI document contained minimal descriptions that provided no parameter guidance or usage examples. ### Step 2: Connect the optimization agent We created a simple Python agent that connects to the Gram MCP server: ```python from pydantic_ai import Agent from pydantic_ai.mcp import MCPServerStreamableHTTP agent = Agent( model='gpt-4o-mini', system_prompt="You help users manage their todo lists efficiently.", toolsets=[ MCPServerStreamableHTTP( url="https://app.getgram.ai/mcp/todo-example" ) ] ) ``` ### Step 3: Collect baseline performance data We tested the agent with realistic user queries: ```python test_queries = [ "Add 'Buy groceries' to my todo list", "Show me all my todos", "Add 'Call dentist' to my todos", "Mark the first todo as completed", "Show me my todos again", "Delete the completed todo" ] ``` Here are the baseline results: - **Success rate:** 83.3% (5 out of 6 queries succeeded) - **Average response time:** 8.36 seconds - **CAPR score:** 0.833 - **Key failure:** The delete operation failed because the agent couldn't determine which todo to delete without specific ID guidance ### Step 4: Run the optimization analysis Using the Bloomberg team's joint optimization framework, we analyzed the interaction data: ```python # Convert our interaction logs to the optimization format optimization_interactions = convert_to_optimization_format(interactions) # Extract current tool descriptions from the OpenAPI spec current_descriptions = extract_tool_descriptions_from_openapi(openapi_spec) # Run the joint optimization optimized_prompt, optimized_tools = joint_optimization( optimization_interactions, current_system_prompt, current_descriptions ) ``` The analysis identified specific improvements for both the system prompt and tool descriptions. For example, here's how the system prompt can be enhanced: - **Before:** "You help users manage their todo lists efficiently." - **After:** "You help users manage their todo lists efficiently. Use specific commands like 'Add', 'Show', 'Update', 'Replace', or 'Delete' followed by the task details. For example, you can say 'Add task X' to create a new todo or 'Show my todos' to list all todos." And here's how tool descriptions can be improved: | Tool | Original Description | Optimized Description | |------|---------------------|----------------------| | `deleteTodo` | "Delete a todo" | "Delete a todo by ID. Example usage: deleteTodo(18) to remove the todo with ID 18." | | `updateTodo` | "Update a todo" | "Update a todo by ID. Example usage: updateTodo(1, 'New details') to change the details of the todo with ID 1." | | `createTodo` | "Create a new todo" | "Create a new todo. Example usage: createTodo('New task description') to add a new task." | | `listTodos` | "List all todos" | "List all todos. Example usage: listTodos() to retrieve all current todos." | ### Step 5: Generate the optimized OpenAPI document The framework automatically generated an updated OpenAPI document with Gram's `x-gram` extensions: ```yaml paths: /todos/{id}: delete: operationId: deleteTodo summary: Delete a todo x-gram: name: deletetodo description: 'Delete a todo by ID. Example usage: deleteTodo(18) to remove the todo with ID 18.' put: operationId: updateTodo summary: Update a todo x-gram: name: updatetodo description: 'Update a todo by ID. Example usage: updateTodo(1, "New details") to change the details of the todo with ID 1.' ``` ### Step 6: Update the Gram server Next, we updated the Gram server configuration. To do this yourself: 1. Log in to the Gram dashboard. 2. Navigate to **Toolsets**. 3. Click on the API Source you're updating, then select **Update** in the dropdown menu. 4. Upload the optimized OpenAPI document. ![Screenshot of the Gram UI showing the API Source update process](/assets/blog/cost-aware-pass-rate/gram-api-source.png) The new descriptions will apply to any MCP servers using tools from the updated API Source. ### Step 7: Test the optimized server After deploying the updated server, we ran the same test queries to measure improvements. Our example query for deleting a todo item performed better with clear parameter guidance, resulting in a higher success rate and faster response time. ## Implementing CAPR and joint optimization for your tools The [Bloomberg research repository](https://github.com/Bingo-W/ToolOptimization) contains the complete code for Bloomberg's CAPR and joint optimization framework, including benchmarks. To implement similar optimizations for your own tools, follow these steps: 1. **Analyze your interaction data:** Collect and analyze user interactions with your tools to identify common failure points and areas for improvement. 2. **Define clear tool descriptions:** Ensure that each tool has a clear and specific description, including example usage patterns. 3. **Incorporate usage examples:** Add usage examples to your tool descriptions to clarify expected input formats and behaviors. 4. **Test and iterate:** Continuously test your tools with real user queries, gather feedback, and iterate on your descriptions and prompts to improve performance. ## Risks when implementing CAPR and joint optimization The research revealed an unexpected insight: Verbalized optimization can overfit just like traditional machine learning. After two to three optimization iterations, tool call counts began to increase again despite maintaining accuracy. There isn't a clear solution to this problem yet, but we recommend tracking efficiency metrics closely to identify potential overfitting early. ## Practical next steps If you're building agents and tools for your organization, consider updating your AI tool usage metrics to take cost into account. This is essential to optimizing for efficiency and effectiveness. If you maintain MCP servers, we recommend implementing similar strategies to optimize your servers' tool descriptions. You could also make it easier for users to customize your tools' descriptions to better fit their specific needs. At Speakeasy, we're following this research closely and building the infrastructure to enable our users to curate, measure, and optimize their MCP servers effectively. # Configure the PokeAPI provider Source: https://speakeasy.com/blog/create-a-terraform-provider-a-guide-for-beginners This tutorial shows you how to create a simple Terraform provider for your web service. Terraform providers let your customers manage your API through declarative configuration files, the same way they manage AWS, Azure, or any other infrastructure. But HashiCorp's official tutorials are lengthy and overwhelming for developers new to provider development. This guide cuts through the complexity, demonstrating how to build a working provider that implements create, read, update, and delete (CRUD) operations against a real API ([PokéAPI](https://pokeapi.co/)). You don't need any experience using Terraform to follow along — we'll explain everything as we go. ## Prerequisites For this tutorial, you need the following installed on your machine: - [Terraform](https://developer.hashicorp.com/terraform/install) - [Go (1.21 or higher)](https://go.dev/doc/install) - [Speakeasy CLI](https://www.speakeasy.com/docs/speakeasy-reference/cli/getting-started) ## Set up your system First, clone the example project. ```bash git clone https://github.com/speakeasy-api/examples cd examples/terraform-provider-pokeapi/base ``` The base project includes the OpenAPI document for the PokéAPI. ### Generate the PokéAPI SDK Terraform providers require writing your own logic to create, read, update, and delete resources via API calls. Writing those HTTP client functions, request-response models, and error handling can take hundreds of lines or repetitive code. Instead, we'll use Speakeasy to generate a type-safe Go SDK that handles all API interactions for us. First, install `staticcheck`, which is required by Speakeasy for linting: ```bash go install honnef.co/go/tools/cmd/staticcheck@latest export PATH=$PATH:$HOME/go/bin ``` To make this permanent, add the export to your shell profile (for example, `.bashrc` or `.zshrc`): ```bash echo 'export PATH=$PATH:$HOME/go/bin' >> ~/.zshrc ``` Generate the SDK from the OpenAPI document using the following command: ```bash speakeasy quickstart ``` Provide the following information as Speakeasy requests it: - **The OpenAPI document location:** `openapi.yml` - **Give your SDK a name:** `PokeAPISDK` - **What you would like to generate:** `Software Development Kit (SDK)` - **The SDK language you would like to generate:** `Go` - **Choose a `modulePath`:** `github.com/pokeapi/sdk` - **Choose a `sdkPackageName`:** `PokeAPISDK` - **The directory the Go files should be written to:** `/path/to/project/poke-api-sdk` This creates a complete SDK in the `poke-api-sdk` directory with built-in HTTP client logic, type-safe models, and error handling. ### Initialize the Go module We're building a provider that connects Terraform to the PokéAPI. This requires two key dependencies. - `terraform-plugin-framework`: The official library for building Terraform providers. - `terraform-plugin-log`: The plugin for diagnostic logging and debugging. Initialize the module following Terraform's naming convention: ```bash go mod init example.com/me/terraform-provider-pokeapi go get github.com/hashicorp/terraform-plugin-framework@latest go get github.com/hashicorp/terraform-plugin-log@latest ``` Add the Speakeasy-generated SDK as a local dependency in `go.mod`: ```diff // go.mod module example.com/me/terraform-provider-pokeapi go 1.24.0 toolchain go1.24.8 + require ( + github.com/pokeapi/sdk v0.0.0-00010101000000-000000000000 +) require ( github.com/hashicorp/terraform-plugin-framework v1.16.1 // indirect github.com/hashicorp/terraform-plugin-log v0.9.0 // indirect ) + replace github.com/pokeapi/sdk => ./poke-api-sdk ``` > **Note:** Adjust the path (`./poke-api-sdk`) to match the actual directory name Speakeasy created. After editing `go.mod`, update the dependency tree: ```bash go mod tidy ``` This command downloads all transitive dependencies and updates `go.sum` with checksums for the modules your provider needs. ## Create the provider files So you have a web service, and in reality, you may even have an SDK in Python, Go, Java, and other languages that your customers could use to call your service. Why do you need Terraform, too? We answer this question in detail in [our blog post about using Terraform as a SaaS API interface](https://www.speakeasy.com/post/build-terraform-providers). In summary, Terraform allows your customers to manage multiple environments with a single service (Terraform) through declarative configuration files that can be stored in Git. This means that if one of your customers wants to add a new user, or a whole new franchise, they can copy a Terraform resource configuration file from an existing franchise, update it, check it into GitHub, and get it approved. Then Terraform can run it automatically using continuous integration. Terraform provides benefits for your customers in terms of speed, safety, repeatability, auditing, and correctness. A Terraform provider consists of three essential files that work together: - An entry point that Terraform calls - A provider configuration that sets up the API client - Resource files that implement CRUD operations ### The entry point Create a `main.go` file in your project root: ```go package main import ( "context" "flag" "log" "example.com/me/terraform-provider-pokeapi/internal/provider" "github.com/hashicorp/terraform-plugin-framework/providerserver" ) var ( version string = "dev" ) func main() { var debug bool flag.BoolVar(&debug, "debug", false, "set to true to run the provider with support for debuggers like delve") flag.Parse() opts := providerserver.ServeOpts{ Address: "example.com/me/pokeapi", Debug: debug, } err := providerserver.Serve(context.Background(), provider.New(version), opts) if err != nil { log.Fatal(err.Error()) } } ``` This file starts the provider server that Terraform connects to. The `Address` is a unique identifier for your provider (like a package name). Users will reference this same address in their Terraform configurations to specify which provider they want to use. For example: ```hcl terraform { required_providers { pokeapi = { source = "example.com/example/pokeapi" } } } ``` ### The provider configuration The provider configuration file configures the provider with the PokéAPI endpoint and initializes the Speakeasy SDK client. Create an `internal/provider/provider.go` file: ```go package provider import ( "context" "github.com/hashicorp/terraform-plugin-framework/datasource" "github.com/hashicorp/terraform-plugin-framework/function" "github.com/hashicorp/terraform-plugin-framework/path" "github.com/hashicorp/terraform-plugin-framework/provider" "github.com/hashicorp/terraform-plugin-framework/provider/schema" "github.com/hashicorp/terraform-plugin-framework/resource" "github.com/hashicorp/terraform-plugin-framework/types" pokeapisdk "github.com/pokeapi/sdk" ) var _ provider.Provider = &PokeAPIProvider{} var _ provider.ProviderWithFunctions = &PokeAPIProvider{} type PokeAPIProvider struct { version string } type PokeAPIProviderModel struct { Endpoint types.String `tfsdk:"endpoint"` } func (p *PokeAPIProvider) Metadata(ctx context.Context, req provider.MetadataRequest, resp *provider.MetadataResponse) { resp.TypeName = "pokeapi" resp.Version = p.version } func (p *PokeAPIProvider) Schema(ctx context.Context, req provider.SchemaRequest, resp *provider.SchemaResponse) { resp.Schema = schema.Schema{ MarkdownDescription: `The PokeAPI provider allows you to manage Pokemon data using the Pokemon API.`, Attributes: map[string]schema.Attribute{ "endpoint": schema.StringAttribute{ MarkdownDescription: "PokeAPI endpoint URL. Defaults to https://pokeapi.co", Optional: true, }, }, } } func (p *PokeAPIProvider) Configure(ctx context.Context, req provider.ConfigureRequest, resp *provider.ConfigureResponse) { var data PokeAPIProviderModel resp.Diagnostics.Append(req.Config.Get(ctx, &data)...) if resp.Diagnostics.HasError() { return } endpoint := "https://pokeapi.co" if !data.Endpoint.IsNull() { endpoint = data.Endpoint.ValueString() } if endpoint == "" { resp.Diagnostics.AddAttributeError( path.Root("endpoint"), "Missing PokeAPI Endpoint", "The provider cannot create the PokeAPI client as there is a missing or empty value for the PokeAPI endpoint.", ) } if resp.Diagnostics.HasError() { return } client := pokeapisdk.New( pokeapisdk.WithServerURL(endpoint), ) resp.DataSourceData = client resp.ResourceData = client } func (p *PokeAPIProvider) Resources(ctx context.Context) []func() resource.Resource { return []func() resource.Resource{ NewPokemonResource, } } func (p *PokeAPIProvider) DataSources(ctx context.Context) []func() datasource.DataSource { return []func() datasource.DataSource{} } func (p *PokeAPIProvider) Functions(ctx context.Context) []func() function.Function { return []func() function.Function{} } func New(version string) func() provider.Provider { return func() provider.Provider { return &PokeAPIProvider{ version: version, } } } ``` The Go file above implements the provider interface with several key functions: - `Schema()` defines the provider's configuration schema with an optional `endpoint` attribute that users can set in their Terraform configs. - `Configure()` initializes the Speakeasy SDK client with `pokeapisdk.New()`, instead of writing dozens of lines of HTTP client setup, and then stores it in `resp.ResourceData`. - `Resources()` registers the `PokemonResource` so Terraform knows which resources this provider manages. - `DataSources()` and `Functions()` return empty lists, since we're not implementing data sources or custom functions in this tutorial. ### The Pokémon resource This is where the complexity begins to show – you must define schemas for every Pokémon attribute and implement CRUD operations. Large APIs with nested objects require hundreds of lines of repetitive schema definitions and type mappings. Create a `internal/provider/pokemon_resource.go` file. Then, create the resources and schemas: ```go // internal/provider/pokemon_resource.go package provider import ( "context" "fmt" "github.com/hashicorp/terraform-plugin-framework/attr" "github.com/hashicorp/terraform-plugin-framework/path" "github.com/hashicorp/terraform-plugin-framework/resource" "github.com/hashicorp/terraform-plugin-framework/resource/schema" "github.com/hashicorp/terraform-plugin-framework/types" pokeapisdk "github.com/pokeapi/sdk" "github.com/pokeapi/sdk/models/operations" ) var _ resource.Resource = &PokemonResource{} var _ resource.ResourceWithImportState = &PokemonResource{} func NewPokemonResource() resource.Resource { return &PokemonResource{} } type PokemonResource struct { client *pokeapisdk.PokeApisdk } type PokemonResourceModel struct { ID types.Int64 `tfsdk:"id"` Name types.String `tfsdk:"name"` Height types.Int64 `tfsdk:"height"` Weight types.Int64 `tfsdk:"weight"` BaseExperience types.Int64 `tfsdk:"base_experience"` Stats types.List `tfsdk:"stats"` Types types.List `tfsdk:"types"` Abilities types.List `tfsdk:"abilities"` Sprites types.Object `tfsdk:"sprites"` } func (r *PokemonResource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { resp.TypeName = req.ProviderTypeName + "_pokemon" } func (r *PokemonResource) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) { resp.Schema = schema.Schema{ MarkdownDescription: "Pokemon resource", Attributes: map[string]schema.Attribute{ "id": schema.Int64Attribute{ MarkdownDescription: "Pokemon ID", Required: true, }, "name": schema.StringAttribute{ MarkdownDescription: "Pokemon name", Computed: true, }, "height": schema.Int64Attribute{ MarkdownDescription: "Height in decimetres", Computed: true, }, "weight": schema.Int64Attribute{ MarkdownDescription: "Weight in hectograms", Computed: true, }, "base_experience": schema.Int64Attribute{ MarkdownDescription: "Base experience", Computed: true, }, "stats": schema.ListNestedAttribute{ MarkdownDescription: "Base stats", Computed: true, NestedObject: schema.NestedAttributeObject{ Attributes: map[string]schema.Attribute{ "base_stat": schema.Int64Attribute{ Computed: true, }, "effort": schema.Int64Attribute{ Computed: true, }, "stat": schema.SingleNestedAttribute{ Computed: true, Attributes: map[string]schema.Attribute{ "name": schema.StringAttribute{Computed: true}, "url": schema.StringAttribute{Computed: true}, }, }, }, }, }, "types": schema.ListNestedAttribute{ MarkdownDescription: "Pokemon types", Computed: true, NestedObject: schema.NestedAttributeObject{ Attributes: map[string]schema.Attribute{ "slot": schema.Int64Attribute{Computed: true}, "type": schema.SingleNestedAttribute{ Computed: true, Attributes: map[string]schema.Attribute{ "name": schema.StringAttribute{Computed: true}, "url": schema.StringAttribute{Computed: true}, }, }, }, }, }, "abilities": schema.ListAttribute{ MarkdownDescription: "Abilities", Computed: true, ElementType: types.ObjectType{ AttrTypes: map[string]attr.Type{ "is_hidden": types.BoolType, "slot": types.Int64Type, "ability": types.ObjectType{ AttrTypes: map[string]attr.Type{ "name": types.StringType, "url": types.StringType, }, }, }, }, }, "sprites": schema.SingleNestedAttribute{ MarkdownDescription: "Sprite images", Computed: true, Attributes: map[string]schema.Attribute{ "front_default": schema.StringAttribute{Computed: true}, "back_default": schema.StringAttribute{Computed: true}, }, }, }, } } func (r *PokemonResource) Configure(ctx context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) { if req.ProviderData == nil { return } client, ok := req.ProviderData.(*pokeapisdk.PokeAPISDK) if !ok { resp.Diagnostics.AddError( "Unexpected Resource Configure Type", fmt.Sprintf("Expected *pokeapisdk.PokeAPISDK, got: %T", req.ProviderData), ) return } r.client = client } ``` The `PokemonResourceModel` struct uses Terraform-specific types (like `types.Int64` and `types.List`) instead of Go's native types, because Terraform needs to track null values and unknown states during planning. The `Schema()` function explicitly defines the structure of each attribute. Notice how nested objects like `stats` require verbose `NestedObject` definitions with their own attribute maps, making this one of the most tedious parts of provider development. Now, add the CRUD operations: ```go // internal/provider/pokemon_resource.go ... func (r *PokemonResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { var data PokemonResourceModel resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...) if resp.Diagnostics.HasError() { return } pokemonID := data.ID.ValueInt64() pokemon, err := r.client.Pokemon.PokemonRead(ctx, pokemonID) if err != nil { resp.Diagnostics.AddError("Client Error", fmt.Sprintf("Unable to read Pokemon: %s", err)) return } if err := r.mapPokemonToState(ctx, pokemon, &data); err != nil { resp.Diagnostics.AddError("Mapping Error", fmt.Sprintf("Unable to map Pokemon: %s", err)) return } resp.Diagnostics.Append(resp.State.Set(ctx, &data)...) } func (r *PokemonResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { var data PokemonResourceModel resp.Diagnostics.Append(req.State.Get(ctx, &data)...) if resp.Diagnostics.HasError() { return } pokemonID := data.ID.ValueInt64() pokemon, err := r.client.Pokemon.PokemonRead(ctx, pokemonID) if err != nil { resp.Diagnostics.AddError("Client Error", fmt.Sprintf("Unable to read Pokemon: %s", err)) return } if err := r.mapPokemonToState(ctx, pokemon, &data); err != nil { resp.Diagnostics.AddError("Mapping Error", fmt.Sprintf("Unable to map Pokemon: %s", err)) return } resp.Diagnostics.Append(resp.State.Set(ctx, &data)...) } func (r *PokemonResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { var data PokemonResourceModel resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...) if resp.Diagnostics.HasError() { return } pokemonID := data.ID.ValueInt64() pokemon, err := r.client.Pokemon.PokemonRead(ctx, pokemonID) if err != nil { resp.Diagnostics.AddError("Client Error", fmt.Sprintf("Unable to read Pokemon: %s", err)) return } if err := r.mapPokemonToState(ctx, pokemon, &data); err != nil { resp.Diagnostics.AddError("Mapping Error", fmt.Sprintf("Unable to map Pokemon: %s", err)) return } resp.Diagnostics.Append(resp.State.Set(ctx, &data)...) } func (r *PokemonResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { // Pokemon can't be deleted from the API, just remove from state } func (r *PokemonResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) { resource.ImportStatePassthroughID(ctx, path.Root("id"), req, resp) } ``` Each CRUD function follows the same pattern: - Get the current state with `req.Plan.Get()` or `req.State.Get()`. - Call the API using the Speakeasy SDK (`r.client.Pokemon.PokemonRead()`). - Map the response to Terraform types. - Save the response with `resp.State.Set()`. Since PokéAPI is read-only, `Create()`, `Read()`, and `Update()` all fetch Pokémon data from the same endpoint, while `Delete()` is a no-op that just removes the resource from Terraform's state file. Now, handle the data mapping: ```go // internal/provider/pokemon_resource.go ... func (r *PokemonResource) mapPokemonToState(ctx context.Context, pokemonResp *operations.PokemonReadResponse, data *PokemonResourceModel) error { if pokemonResp == nil || pokemonResp.Object == nil { return fmt.Errorf("received nil response from API") } pokemon := *pokemonResp.Object if pokemon.ID != nil { data.ID = types.Int64Value(int64(*pokemon.ID)) } if pokemon.Name != nil { data.Name = types.StringValue(*pokemon.Name) } if pokemon.Height != nil { data.Height = types.Int64Value(int64(*pokemon.Height)) } if pokemon.Weight != nil { data.Weight = types.Int64Value(int64(*pokemon.Weight)) } if pokemon.BaseExperience != nil { data.BaseExperience = types.Int64Value(int64(*pokemon.BaseExperience)) } // Map stats statsElements := []attr.Value{} if pokemon.Stats != nil { for _, stat := range pokemon.Stats { if stat.BaseStat == nil || stat.Effort == nil || stat.Stat.Name == nil || stat.Stat.URL == nil { continue } statObj, _ := types.ObjectValue( map[string]attr.Type{ "base_stat": types.Int64Type, "effort": types.Int64Type, "stat": types.ObjectType{ AttrTypes: map[string]attr.Type{ "name": types.StringType, "url": types.StringType, }, }, }, map[string]attr.Value{ "base_stat": types.Int64Value(*stat.BaseStat), "effort": types.Int64Value(*stat.Effort), "stat": types.ObjectValueMust( map[string]attr.Type{ "name": types.StringType, "url": types.StringType, }, map[string]attr.Value{ "name": types.StringValue(*stat.Stat.Name), "url": types.StringValue(*stat.Stat.URL), }, ), }, ) statsElements = append(statsElements, statObj) } } statsList, _ := types.ListValue( types.ObjectType{ AttrTypes: map[string]attr.Type{ "base_stat": types.Int64Type, "effort": types.Int64Type, "stat": types.ObjectType{ AttrTypes: map[string]attr.Type{ "name": types.StringType, "url": types.StringType, }, }, }, }, statsElements, ) data.Stats = statsList // Map types typesElements := []attr.Value{} if pokemon.Types != nil { for _, typeInfo := range pokemon.Types { if typeInfo.Slot == nil || typeInfo.Type.Name == nil || typeInfo.Type.URL == nil { continue } typeObj, _ := types.ObjectValue( map[string]attr.Type{ "slot": types.Int64Type, "type": types.ObjectType{ AttrTypes: map[string]attr.Type{ "name": types.StringType, "url": types.StringType, }, }, }, map[string]attr.Value{ "slot": types.Int64Value(*typeInfo.Slot), "type": types.ObjectValueMust( map[string]attr.Type{ "name": types.StringType, "url": types.StringType, }, map[string]attr.Value{ "name": types.StringValue(*typeInfo.Type.Name), "url": types.StringValue(*typeInfo.Type.URL), }, ), }, ) typesElements = append(typesElements, typeObj) } } typesList, _ := types.ListValue( types.ObjectType{ AttrTypes: map[string]attr.Type{ "slot": types.Int64Type, "type": types.ObjectType{ AttrTypes: map[string]attr.Type{ "name": types.StringType, "url": types.StringType, }, }, }, }, typesElements, ) data.Types = typesList // Create empty collections for other fields abilitiesEmpty, _ := types.ListValue( types.ObjectType{ AttrTypes: map[string]attr.Type{ "is_hidden": types.BoolType, "slot": types.Int64Type, "ability": types.ObjectType{ AttrTypes: map[string]attr.Type{ "name": types.StringType, "url": types.StringType, }, }, }, }, []attr.Value{}, ) data.Abilities = abilitiesEmpty spritesEmpty, _ := types.ObjectValue( map[string]attr.Type{ "front_default": types.StringType, "back_default": types.StringType, }, map[string]attr.Value{ "front_default": types.StringNull(), "back_default": types.StringNull(), }, ) data.Sprites = spritesEmpty return nil } ``` The `mapPokemonToState()` function converts the SDK's response types to the Terraform type system. For each nested list like `stats`, you need to: - Define the exact `AttrTypes` structure that matches your schema - Iterate through the API response, checking for nil values - Construct properly typed objects using `types.ObjectValue()` In this manual, repetitive mapping consumes most of the provider development time, and any mismatch between the `AttrTypes` map here and the schema definition can cause cryptic runtime type errors. ## Run the provider At this point, you have: - Generated an SDK with Speakeasy to handle API calls to PokéAPI - Created three provider files, an entry point (main.go), a provider configuration (`provider.go`), and a Pokémon resource with CRUD operations (`pokemon_resource.go`) - Implemented complex data mapping that links API responses to Terraform state Now it's time to test the provider by pretending you're a user who wants to manage Pokémon data through Terraform. ### Configure local development Because you haven't published your provider to the Terraform registry yet, you need to tell Terraform to use your local build. Create a `.terraformrc` file in your home directory: ```bash cat > ~/.terraformrc << 'EOF' provider_installation { dev_overrides { "example.com/me/pokeapi" = "/absolute/path/to/your/project/bin" } direct {} # For all other providers, install directly from their origin provider. } EOF ``` The `dev_overrides` setting tells Terraform to use your local binary instead of downloading from the registry. Replace `/absolute/path/to/your/project/bin` with the actual absolute path to your project's bin directory. For example: `/Users/yourusername/terraform-provider-pokeapi/bin`. ### Build the server Compile the provider into an executable binary: ```bash go build -o ./bin/terraform-provider-pokeapi ``` The command above creates the provider plugin that Terraform will call to manage Pokémon resources. ### Create a test configuration Create an `examples` directory to hold your Terraform configuration: ```bash mkdir -p examples cd examples ``` Create a `main.tf` file inside the `examples` directory. This is what a user of your provider would write: ```tf terraform { required_providers { pokeapi = { source = "example.com/me/pokeapi" } } } provider "pokeapi" { endpoint = "https://pokeapi.co" } resource "pokeapi_pokemon" "pikachu" { id = 25 # Pikachu's ID } resource "pokeapi_pokemon" "charizard" { id = 6 # Charizard's ID } output "pikachu_name" { description = "The name of Pikachu" value = pokeapi_pokemon.pikachu.name } output "pikachu_height" { description = "Pikachu's height in decimetres" value = pokeapi_pokemon.pikachu.height } output "pikachu_weight" { description = "Pikachu's weight in hectograms" value = pokeapi_pokemon.pikachu.weight } output "pikachu_base_experience" { description = "Base experience gained from defeating Pikachu" value = pokeapi_pokemon.pikachu.base_experience } output "pikachu_stats" { description = "Pikachu's base stats - this is a complex nested list" value = pokeapi_pokemon.pikachu.stats } output "pikachu_types" { description = "Pikachu's types - this is a nested list with slot and type information" value = pokeapi_pokemon.pikachu.types } output "charizard_name" { description = "Charizard's name" value = pokeapi_pokemon.charizard.name } ``` This configuration tells Terraform to fetch data for two Pokémon (Pikachu and Charizard) and output some of their attributes. ### Apply the configuration Run Terraform to execute the configuration: ```bash terraform plan # Preview what Terraform will do terraform apply -auto-approve # Apply the changes ``` Terraform should output the following: ```txt Terraform used the selected providers to generate the following execution plan. Resource actions are indicated with the following symbols: + create Terraform will perform the following actions: # pokeapi_pokemon.charizard will be created + resource "pokeapi_pokemon" "charizard" { + abilities = (known after apply) + base_experience = (known after apply) + cries = (known after apply) + game_indices = (known after apply) + height = (known after apply) + held_items = (known after apply) + id = 6 + is_default = (known after apply) + location_area_encounters = (known after apply) + moves = (known after apply) + name = (known after apply) + order = (known after apply) + past_abilities = (known after apply) + past_types = (known after apply) + species = (known after apply) + sprites = (known after apply) + stats = (known after apply) + types = (known after apply) + weight = (known after apply) } # pokeapi_pokemon.pikachu will be created + resource "pokeapi_pokemon" "pikachu" { + abilities = (known after apply) + base_experience = (known after apply) + cries = (known after apply) + game_indices = (known after apply) + height = (known after apply) + held_items = (known after apply) + id = 25 + is_default = (known after apply) + location_area_encounters = (known after apply) + moves = (known after apply) + name = (known after apply) + order = (known after apply) + past_abilities = (known after apply) + past_types = (known after apply) + species = (known after apply) + sprites = (known after apply) + stats = (known after apply) + types = (known after apply) + weight = (known after apply) } Plan: 2 to add, 0 to change, 0 to destroy. Changes to Outputs: + charizard_name = (known after apply) + pikachu_base_experience = (known after apply) + pikachu_height = (known after apply) + pikachu_name = (known after apply) + pikachu_stats = (known after apply) + pikachu_types = (known after apply) + pikachu_weight = (known after apply) pokeapi_pokemon.pikachu: Creating... pokeapi_pokemon.charizard: Creating... pokeapi_pokemon.pikachu: Creation complete after 0s [name=pikachu] pokeapi_pokemon.charizard: Creation complete after 0s [name=charizard] Apply complete! Resources: 2 added, 0 changed, 0 destroyed. Outputs: charizard_name = "charizard" pikachu_base_experience = 112 pikachu_height = 4 pikachu_name = "pikachu" pikachu_stats = tolist([ { "base_stat" = 35 "effort" = 0 "stat" = { "name" = "hp" "url" = "https://pokeapi.co/api/v2/stat/1/" } }, { "base_stat" = 55 "effort" = 0 "stat" = { "name" = "attack" "url" = "https://pokeapi.co/api/v2/stat/2/" } }, { "base_stat" = 40 "effort" = 0 "stat" = { "name" = "defense" "url" = "https://pokeapi.co/api/v2/stat/3/" } }, { "base_stat" = 50 "effort" = 0 "stat" = { "name" = "special-attack" "url" = "https://pokeapi.co/api/v2/stat/4/" } }, { "base_stat" = 50 "effort" = 0 "stat" = { "name" = "special-defense" "url" = "https://pokeapi.co/api/v2/stat/5/" } }, { "base_stat" = 90 "effort" = 2 "stat" = { "name" = "speed" "url" = "https://pokeapi.co/api/v2/stat/6/" } }, ]) pikachu_types = tolist([ { "slot" = 1 "type" = { "name" = "electric" "url" = "https://pokeapi.co/api/v2/type/13/" } }, ]) pikachu_weight = 60 ``` Terraform called your provider's `Create()` function for each Pokemon resource, which: - Made an API call to PokéAPI using the Speakeasy SDK - Mapped the API response to Terraform's type system - Stored the Pokemon data in `terraform.tfstate` Check the state file `terraform.tfstate` that was created. It will look similar to the following example: ```txt { "version": 4, "terraform_version": "1.9.5", "serial": 3, "lineage": "317ebf1d-403e-03fa-787e-4694386c5acd", "outputs": { "charizard_name": { "value": "charizard", "type": "string" }, "pikachu_abilities": { "value": [], "type": [ "list", [ "object", ... } ``` This file contains Terraform's view of the Pokémon resources. Never edit the state file manually; always let Terraform manage it. ### Debugging tips If you need to debug the provider, enable logging: ```bash export TF_LOG=DEBUG terraform apply ``` You can also add logging in your provider code: ```go import "github.com/hashicorp/terraform-plugin-log/tflog" // Inside any function tflog.Info(ctx, "Reading Pokemon", map[string]any{"id": pokemonID}) ``` To push the current project a bit more, try modifying your configuration to see how Terraform handles changes: - Add more Pokémon resources with different IDs. - Run `terraform plan` to see what would change. - Run `terraform destroy` to remove resources from state. ## Limitations and further reading You now have a working minimal example of a Terraform provider, but it isn't ready for production use yet. We recommend first enhancing it with features such as the following: - **Markup responses (JSON or XML):** PokéAPI returns JSON, which simplified our implementation. However, your API might return XML, Protobuf, or custom formats that require additional parsing logic. You need to handle serialization, deserialization, and error responses for the format your service uses. - **Versioning and continuous integration:** Your web service will change over time, and the provider needs to change to match it. Your customers will need to use the correct versions of both the web service and the provider. We also recommend automatically building and releasing your provider from GitHub, using GitHub Actions. - **Testing:** A real web service is complex, and you need to write a lot of integration tests to ensure that every provider version you release does exactly what it's supposed to when calling the service. - **Documentation:** Your customers want to know exactly how to set up and configure your provider to manage whatever resources your service offers. - **Publishing the provider to the Terraform registry:** Until you add metadata to your provider and release it in the Terraform ecosystem, no one can use it. You can also add additional functionality, like handling data sources (which are different from resources) and external imports of resources. If you want to learn how to enhance your provider, the best place to start is the official [Terraform provider creation tutorial](https://developer.hashicorp.com/terraform/tutorials/providers-plugin-framework/providers-plugin-framework-provider). You can also clone the [provider scaffolding repository](https://github.com/hashicorp/terraform-provider-scaffolding-framework) and read through it to see how Terraform structures a provider and uses `.github` to offer continuous integration. Once you've worked through the tutorial, we recommend reading about [how Terraform works with plugins](https://developer.hashicorp.com/terraform/plugin/how-terraform-works). HashiCorp released an [automated provider generator](https://developer.hashicorp.com/terraform/plugin/code-generation) in technical preview during 2024, but development has stalled, and it doesn't handle API operation logic or data type conversions - you still write most code manually. We cover this tool and production-ready alternatives in our article on [building Terraform providers](https://www.speakeasy.com/blog/how-to-build-terraform-providers). ## A simpler way You might feel that creating and maintaining your own Terraform provider is far too much work when you're busy trying to run a business and provide your core service. Luckily, there is a much easier alternative. We at Speakeasy are passionate about and dedicated to making web APIs easy for customers to use. Our service can automatically generate a complete Terraform provider with documentation that's ready for you to offer to your customers on the Terraform registry. All you need is an OpenAPI document for your service and a few custom attributes. Read our [guide to generating a Terraform provider with Speakeasy](https://www.speakeasy.com/blog/how-to-build-terraform-providers) and see how we can massively reduce your workload by setting up a Terraform provider in a few clicks. # custom-code-regions-an-overlay-playground Source: https://speakeasy.com/blog/custom-code-regions-an-overlay-playground import { Callout, ReactPlayer } from "@/lib/mdx/components"; 🎉 Welcome to our first changelog of 2025! This update focuses on customization, introducing powerful tools like Custom Code Regions for direct SDK enhancements and the Overlay Playground, a new open-source tool for modular spec-level changes. These features, combined with other improvements and fixes, make tailoring your SDKs and OpenAPI workflows easier than ever. Let's explore what's new! ## Custom Code Regions ```typescript filename="codeRegions.ts" // !mark gold // #region imports import chalk from "chalk"; import { ClientSDK } from "./lib/sdks.js"; // !mark gold // #endregion imports class Acme extends ClientSDK { // ... generated code ... // !mark gold // #region sdk-class-body greet(name: string): string { return chalk.green(`Hello, ${name}!`); } // !mark gold // #endregion sdk-class-body } ``` **Custom Code Regions** give you the flexibility to embed custom logic directly into your SDK without modifying the OpenAPI spec. Using foldable regions in your codebase, you can add anything from helper methods to third-party integrations. Your customizations persist across regenerations, keeping your work intact and formatted. ✨ **Why choose Custom Code Regions?** - **Direct control**: Insert your logic directly into the SDK. - **Seamless updates**: Custom code remains untouched during regenerations. - **Ultimate flexibility**: From logging to integrations, you can do anything. ### **How Does Everything Fit Together?** With **Custom Code Regions**, **Hooks**, and **Overlays**, Speakeasy offers three distinct ways to customize your SDKs: | **Customization Tool** | **Purpose** | **Key Use Case** | | ----------------------- | ---------------------------------------------------------------------- | ------------------------------------------------------------------------ | | **Overlays** | Modify the OpenAPI spec for structural or schema-related changes. | Adding paths, tweaking parameters, or updating models. | | **Hooks** | Intercept and modify SDK generation programmatically during the build. | Dynamic updates, injecting logic at specific stages. | | **Custom Code Regions** | Insert custom logic directly into the generated SDK code at runtime. | Adding helper methods, third-party integrations, or SDK-specific tweaks. | Each tool offers unique capabilities, and together, they provide unparalleled flexibility for tailoring your SDKs to your needs. > 📖 Learn more in our release post: > 🔗 [Custom Code Regions: Ultimate SDK Customization](/post/release-custom-code-regions) > 📚 Dive into the documentation: > 🔗 [Custom Code Regions Documentation](/docs/customize/code/code-regions/overview) --- ## Overlay Playground The [**Overlay Playground**](https://overlay.speakeasy.com) is an open-source tool for managing OpenAPI overlays in a more user-friendly way. Instead of dealing with raw YAML or JSON, you can create, edit, and validate overlays through a visual interface with real-time previews. Once you're done, export your overlays and integrate them into your workflows or CI/CD pipeline. Whether you work solo or with a team, the Playground can reduce the complexity of overlay management.
#### **Key Features** - **Interactive Editor**: A visual editor for overlays with real-time updates. - **Validation**: Ensures overlays conform to OpenAPI standards. - **Export and Share**: Save overlays as reusable `.overlay.yaml` files. - **Open Source**: Fully extensible and open for contributions at [github.com/speakeasy-api/jsonpath](https://github.com/speakeasy-api/jsonpath). Start customizing your OpenAPI specs today with the Overlay Playground at [overlay.speakeasy.com](https://overlay.speakeasy.com). 🌟 --- ## 🐝 New Features and Bug Fixes 🐛 Based on the most recent CLI version: [**Speakeasy v1.493.8**](https://github.com/speakeasy-api/openapi-generation/releases/tag/v2.493.8) ### Generation Platform 🐝 Feat: enabled feature flag-based licensing 🐝 Feat: defaulted to API key authentication when a security scheme is missing 🐛 Fix: resolved test generation issues and improved enum handling ### Python 🐝 Feat: upgraded to Poetry 2.0 for modern dependency management 🐝 Feat: bumped minimum Python version to 3.9 🐝 Feat: exposed SDK versioning data as constants 🐛 Fix: added missing imports for per-operation servers 🐛 Fix: updated Mypy for Python 3.13 compatibility ### TypeScript 🐝 Feat: added support for asymmetric webhook signatures 🐛 Fix: improved webhook security parsing ### Go 🐛 Fix: standardized deprecated comments for linter compatibility ### Java 🐝 Feat: added support for JUnit test report generation ### Ruby 🐝 Feat: updated bundler for Ruby 3.4 support # custom-type-support-in-terraform Source: https://speakeasy.com/blog/custom-type-support-in-terraform import { Callout } from "@/lib/mdx/components"; Terraform providers require robust type handling to ensure consistent, reliable infrastructure management. Today, we're excited to announce custom type support in Speakeasy-generated Terraform providers. Custom types enable enhanced validation, improved data handling consistency, and seamless integration with specialized Terraform type libraries. This feature allows API providers to leverage the full power of the terraform-plugin-framework's custom type system while maintaining the simplicity and automation benefits of Speakeasy's SDK generation. --- ## How it works When you define an API schema field that requires specialized validation or handling, you can now use the OpenAPI Specification extension `x-speakeasy-terraform-custom-type` to specify custom type implementations. This extension allows you to define the necessary imports, schema type, and value type for your custom implementation: ```yaml ipv4_address: type: string x-speakeasy-terraform-custom-type: imports: - github.com/hashicorp/terraform-plugin-framework-nettypes/iptypes schemaType: "iptypes.IPv4AddressType{}" valueType: iptypes.IPv4Address ``` The schema implementation automatically enables the custom type's built-in validation during Terraform planning, while maintaining compatibility with your Go SDK's type system. This approach provides the best of both worlds: robust validation at the Terraform layer and seamless data handling in your API integration code. --- ## Key improvements This release includes several significant enhancements to our Terraform generation: - Replaced the terraform-plugin-framework `Number` schema and value types with `Float64` for the generator `number` type, creating a more consistent 1:1 mapping between Terraform and Go SDK types - Added initial custom type support for framework base types including `Bool`, `Float32`, `Float64`, `Int32`, `Int64`, `List`, `Map`, `Set` and `String` - Polished SDK-to-Terraform code conversion for various primitive types including `date`, `date-time`, `int32`, and `number` - Improved variable naming within deeply nested types for better code readability --- ## Getting started To leverage custom type support in your Terraform providers: 1. Identify fields in your API schema that would benefit from specialized validation or handling 2. Add the `x-speakeasy-terraform-custom-type` extension to those fields in your OpenAPI Specification document directly or via OpenAPI Overlay 3. Regenerate your Terraform provider using the Speakeasy CLI For more information on custom types in Terraform, see the [official Terraform plugin framework documentation](https://developer.hashicorp.com/terraform/plugin/framework/handling-data/types/custom). --- ## 🛠️ New Features and Bug Fixes 🐛 Based on the most recent CLI version: [**Speakeasy v1.524.0**](https://github.com/speakeasy-api/speakeasy/releases/tag/v1.524.0) ### Terraform 🐝 **Feat:** Added custom type support for Terraform providers via `x-speakeasy-terraform-custom-type` extension.\ 🐝 **Feat:** Replaced `Number` with `Float64` type for improved data handling consistency.\ 🐝 **Feat:** Enhanced primitive type conversion for `date`, `date-time`, `int32`, and `number` types.\ 🐝 **Feat:** Improved variable naming within deeply nested types. # customisable-imports-openapi-overlays-and-terraform-generation-improvements Source: https://speakeasy.com/blog/customisable-imports-openapi-overlays-and-terraform-generation-improvements Welcome to another edition of the Speakeasy Changelog. In this issue, we'll give you a sneak peek into our support for [OpenAPI Overlays](https://github.com/OAI/Overlay-Specification) and how we're leveraging them to help customers customize their SDKs and other generated artifacts without changing the underlying specification. We'll also be diving into new DevEx improvements that let you customize SDK imports, as well as exciting Terraform releases, including Pulumi support! Sound good? Ok, let's go! 🚀 ## OpenAPI Overlays What is an Overlay, you ask? You can think of them as miniature OpenAPI documents that can be used to customize certain details of your API without altering the source document. Why would you want to maintain one? - You might want to customize your OpenAPI spec for SDK creation, but changing your spec is hard because it's generated from an API framework like FastAPI, tsoa, JOI, etc. - You have a lot of teams at your company creating OpenAPI specs, and asking one of them to make a change is a tough process. - You are setting up a Terraform provider for your OSS product and need different server URLs configured so users only hit a hosted API. Let's look at an example. Here's a simple spec for the Speakeasy bar with only two `tags` defined, `drinks` and `orders`. ```yaml openapi: 3.1.0 info: title: The Speakeasy Bar version: 1.0.0 summary: A bar that serves drinks. servers: - url: https://speakeasy.bar description: The production server. security: - apiKey: [] tags: - name: drinks description: The drinks endpoints. - name: orders description: The orders endpoints. paths: /dinner/: post: ... get: ... /drinks/: post: ... ``` To make an easy-to-use SDK, we've decided that a public interface should use `snacks`, i.e., `sdk.snacks.get_orders()`. As the owner of the company's SDK, you want to make this change, but that would mean making an actual code change with the team that owns the drinks and orders service. Worse yet, it's all microservices, and there is no one team that owns all the services. You can get around this sustainably with an overlay. This overlay includes a `target` that you want to modify in your source document and an `action` to modify the target. ```yaml overlay: 1.0.0 info: title: Overlay to fix the Speakeasy bar version: 0.0.1 actions: - target: "$.tags" description: Add a Snacks tag to the global tags list update: - name: Snacks description: All methods related to serving snacks - target: "$.paths['/dinner']" description: Remove all paths related to serving dinner remove: true ``` Specify that we want to add a new tag to the global tags list `$.tags` and add a description of the edit you're making. Under the update label, add the name and description of the tag you want to add. Now you can use the Speakeasy CLI to merge these two documents right before generating the SDK: ```bash speakeasy overlay apply -s openapi.yaml -o overlay.yaml >> combined.yaml ``` Time to celebrate 🎉 You've just created a new OpenAPI document that you can use to generate an SDK with the `snacks` tag. More on how to use Overlays [here](/docs/prep-openapi/overlays/create-overlays). ## 📦 Customizable Imports At Speakeasy, we believe that automated doesn't mean no input. Certain aspects of SDK design need to be in the hands of the API builders. That's why we've built a platform which is flexible enough to let developers craft the devex for their users. To that end, we've released customizable imports. You can now configure the structure of the `import` paths in your SDK, and how they are referenced by your users. As an example, for Typescript: By default, our SDKs have created models in directories dictated by the OpenAPI spec: - `models/components` - `models/operations` - `models/errors` ```yaml sdk/ ├─ models/ │ ├─ components/ │ │ ├─ user.ts │ │ ├─ drink.ts │ │ └─ ... │ ├─ operations/ │ │ ├─ getuser.ts │ │ ├─ updateuser.ts │ │ ├─ getdrink.ts │ │ ├─ updatedrink.ts │ │ └─ ... │ └─ errors/ │ ├─ sdkerror.ts │ ├─ responseerror.ts │ └─ ... └─ ... ``` This is great for keeping your SDK organized, but it could be a bit verbose for your users, especially for less structured languages like Typescript and Python where global imports are the norm. The import structure in this case would look like: ```typescript import { SDK } from "@speakeasy/bar"; import { User } from "@speakeasy/bar/dist/models/components/user"; import { GetDrinkRequest } from "@speakeasy/bar/dist/models/operations/user"; ``` As an API producer, you can now configure your SDK to generate models into a single directory and import them from there. For Typescript, this would result in: ```yaml / ├─ src │ ├─ models/ │ │ ├─ user.ts │ │ ├─ drink.ts │ │ ├─ getuser.ts │ │ ├─ updateuser.ts │ │ ├─ getdrink.ts │ │ ├─ updatedrink.ts │ │ ├─ sdkerror.ts │ │ ├─ responseerror.ts │ │ ├─ index.ts │ │ └─ ... │ └─ ... └─ ... ``` This results in an import structure that is flat and supports a global path as follows: ```typescript import { GetDrinkRequest, SDK, User } from "@speakeasy/bar"; ``` More documentation on how to configure this in your SDK's `gen.yaml` file can be found [here](/docs/customize-sdks/imports). ## 🚢 Improvements and Bug Fixes 🐛 #### [Speakeasy v119.1](https://github.com/speakeasy-api/speakeasy/releases/tag/v1.119.1) ### Terraform 🚢 Pulumi support for generated Terraform providers\ 🚢 Importing resources with Integer IDs\ 🚢 DataSources with no required arguments (Empty Datasources)\ 🚢 `x-speakeasy-conflicts-with` extension ### Python 🚢 Oauth support for Python SDKs ### Php 🚢 Support for customizable imports ### Typescript 🐛 Ensure `contentType` variable doesn't conflict with common parameter names\ 🐛 Correctly handle `x-www-form-urlencode` in Typescript\ 🚢 unset baseURL on default axios client ### Golang 🐛 `BigInt` & `Decimal` type support within a `Union` type ### Other: 🚢 Allow optional properties in usage snippets to be conditionally rendered\ 🚢 Support for customizing input/output model suffixes\ 🚢 Maintain OpenAPI Order in generated models\ 🚢 Automatic Authentication documentation generated in READMEs\ 🚢 Automatic Retry documentation generated in READMEs when retry extensions are used # definitive-guide-to-devex-portals Source: https://speakeasy.com/blog/definitive-guide-to-devex-portals import { ReactPlayer } from "@/mdx/components"; ## Why You Need A Portal If You Want to Serve Developers When you're teaching a kid to drive, you don't hand them a car key, the operation manual for the car and then leave them to figure it out. Of course you could, but not everyone would get the car moving, and there might be some easily avoided accidents along the way. But this is exactly how many companies teach people to use their APIs: they hand over a stack of documentation, an API key and call it a day. Of course, those tools are important; without them, your users are stuck at ‘Go'. But these tools alone don't really deliver any sort of experience to users. It's basic. It's lackluster. It's _an_ experience, but probably not **_THE_** developer experience you want to give your users. It's certainly not a delightful experience or memorable enough to tell others about. And that lackluster experience has a material impact on the adoption and usage of your API. If developer experience has been underinvested in, a common set of issues begin to pop up: - The average time gap between user signup and first successful request is longer than one day. - Your team spends hours every week troubleshooting client integrations. - The average number of API calls & endpoints made by users isn't expanding over time. Resulting in: - Decreased API adoption and usage - A higher cost to support for each user. - A reduced LTV for each user. To address these issues you need to vastly improve your API's DevEx. **The key to a great user experience is providing your users with an API DevEx Portal** which makes your API: - **Accessible**: there is zero friction to begin sending API requests. - **Understandable**: users are able to get immediate feedback on their API usage – and to self-service troubleshoot issues when they do occur. - **Usable**: It is trivially easy for users to discover and test out new use cases for your API that naturally expand their usage. But what tooling gets you to this point? Let's walk through each of the above criteria and discuss the specific tools that can help you give your API users the DevEx they deserve. ## What Tooling Does Your API Portal Need ### Accessible Making the API accessible means making it as easy as possible for users to make that first API call. 99% of companies are still stuck somewhere in this first stage. **Key Capabilities**: - **API Documentation** - There has been a pervasive view that documentation equals great DevEx. We think that documentation is only the first step: it's critical, but on its own it will still leave your API users wanting. Documentation should be comprehensive, easy to navigate / search, and have code snippets / examples embedded. Ideally, docs should enable users to try the API without any additional tooling or configuration. API docs should also differentiate between API reference (a full list of all endpoints, parameters, response codes, etc.) and usage guides (tutorials that take users step-by-step through how they can use the API to accomplish key tasks). - **Auth Login** - If you want to offer developers a more personalized experience, auth is the prerequisite. You need to know who someone is before you can issue them an API key, and start giving them tools to help them understand and track their usage. Login should of course be managed by a single shared identity service e.g. auth0 or other system of record – you don't want to build a separate user management system for your application and your API for example. - **API Key Management** - Nobody wants to have to fill in a typeform and wait for customer support to review before they can get started with an API. If there's no way for a developer to create keys on their own, most will never convert into users of your product. By the time someone has reviewed their access request, they will have moved on to a new priority, or found an alternative solution. If the API interfaces with sensitive data, and a review process is a legal requirement for production credentials, then enable users to provision sandbox keys without review (more on Sandboxes below).
Key management in Speakeasy's API Portal

### Understandable Even companies where APIs are the primary surface area often struggle to make the investment required to advance their developer portal to being understandable. **Key Capabilities:** - **API Request Viewer** - When debugging, there's no substitute for being able to step through things one at a time. A request viewer makes it possible for your users to view the full list of requests they've sent to your API – without creating additional work for your team to pull logs, send screenshots via email or Slack, or jump on a Zoom call. Without a self-service request viewer, broken integrations create poor API experience and leads to churned clients. A request viewer should provide users the ability to filter by time, response code, endpoint and more, and ideally allow users to edit and replay the request for quick debugging.
Request Viewer in Speakeasy's API Portal

- ‍**API Usage Metrics** - A request viewer is only useful to developers if they know there's an issue to investigate. That is why it's important to surface key usage metrics in real time – so that users know the overall health of their integration. Usage metrics should place an emphasis on error reporting and significant changes in usage so that your users can take corrective action to any errors or unintended changes.
Usage Dashboard in Speakeasy's API Portal

- **API Status Page** - Developers need a place to check if APIs are experiencing downtime. Nothing is more frustrating than having to email a company, “is your API working?” An API status page brings transparency, and transparency is important for building trust with your users. ### Usable Usability tooling is focused on making it easy to test out new API use cases and also making those new use cases easy to find. Usability tooling shines as APIs become larger. Early on an API will serve a single use case, and documentation will focus on supporting that use case. As the API's surface area grows, documentation becomes denser, and isolating relevant instructions becomes challenging. Usability tooling will help insulate users against this by providing structure for the documentation, and making it easier to test out new use cases. **Key Capabilities:** - **Client SDKs** - You need to meet your developers where they already are. Providing client SDKs makes it easier for developers to get started with your API by grounding them in the familiarity of their favorite language, and significantly reducing the amount of boilerplate they need to write. This is especially true if your SDKs can handle auth, pagination, and retries and others. They are therefore great at helping maximize your audience while minimizing support costs. But it's not enough to have SDKs, it's critical that the SDKs are developer-friendly, meaning that they are language idiomatic and human readable. Unfortunately, creating client SDKs is prohibitively expensive for most API teams, since they need to be created and updated by hand. While open source generators exist, the SDKs they output are often buggy and not ergonomic. - **API Runbooks** - We think of runbooks as live usage guides. They take users step-by-step through the process of using your API to accomplish specific tasks, but also show relevant, live API requests in real-time. This helps to focus developers on the key use cases required to complete API integrations. Your customers can use them to grow their usage of your API. As an API builder, runbooks also help you understand the maturity of your customer base: you can begin to understand your API usage as a customer funnel, and start to measure where and why users drop out of the funnel. - **API Sandbox** - Probably nothing helps more with adoption than giving developers an easy way to play with your API. A sandbox can give prospective users a way to use your APIs without needing to sign up for an account. Developers are more likely to trust an API if they've seen it working before needing to hand over their information. And a sandbox can give existing users a way to learn by doing, and without any risk of corrupting production workflows. This enables users to easily expand their use cases for your API. ## How to get to Best-In-Class: Build or Buy? The list above is presented as a rough roadmap. To improve your DevEx, just build each of the tools listed above in order, and you can progress from having no tooling, to having a great Developer Portal. But as any PM or engineer will tell you, knowing what to build is only the beginning. Finding the resources required to build is the real battle. Great DevEx is extremely important for a successful API, but building all of the above is a huge commitment of resources, requires significant ongoing maintenance, and likely isn't a core competency for your organization. As a result, investing in Developer Experience continues to be the project that is slated for next quarter. For almost every company therefore, investing in a best-of-breed solution makes more sense. With an API DevEx Portal from a company like Speakeasy, your customers get a world-class API Developer Experience in days instead of quarters, your product roadmap has negligible impact, and your eng teams don't need to reinvent the wheel. Furthermore, our product is designed with maximum flexibility in mind, giving you the best of both worlds. Every tool in our API DevEx Portal is a React component, and can be customized, branded and extended as you need. Similarly, our platform can be self-hosted or run in our Speakeasy Cloud depending on your requirements. ## Final Thoughts For a long time, companies have been able to get by with substandard developer experiences, but that is beginning to change. Developer Experience is now getting the attention it deserves, and we are rapidly reaching an inflection point. What has been previously considered great DevEx is becoming table stakes for developer products. We know that DevEx isn't ignored because companies don't see the value. Rather, it's the result of painful prioritization decisions. That's why Speakeasy exists. We don't want anyone to have to ever make that tradeoff. With Speakeasy you can get a best-in-class, composable developer portal up and running in a matter of minutes. If you want to learn more, [come chat with us in our Slack](https://go.speakeasy.com/slack)! # devex-portals-as-a-service Source: https://speakeasy.com/blog/devex-portals-as-a-service If there's a time gap between your user signup and first successful request, if your team has to dig through request logs to troubleshoot integrations, if you're not seeing users' API usage expand over time, then you probably need to consider investing in your API developer experience. Our new API DevEx portal makes it easy. We've assembled Speakeasy's individual embeds into an end-to-end nextjs app with the tooling needed to enable your users to: - Manage their API keys - Analyze request and response logs - Understand their API usage - Test new use cases If you're interested in using the portal builder [Come talk to us on Slack!](https://go.speakeasy.com/slack) ## New Features **API DevEx Portal Builder** - We talked two weeks ago about how Client SDKs are a pillar of good API DevEx. Portals are another. If APIs are the powerful bare metal capabilities you offer to your clients, developer experience portals are the packaging that make them an easy-to-use product. A well-built portal unlocks fast initial integration, seamless usage expansion, and self-service troubleshooting by making your API: - **Accessible**: there is zero friction to begin sending API requests. - **Understandable**: users are able to get immediate feedback on their API usage – and to self-service troubleshoot issues when they do occur. - **Usable**: It is trivially easy for users to discover and test out new use cases for your API that naturally expand their usage. See it in action! We take care of the API developer experience, so you can focus on the stuff that makes your API special. # dynamic-dev-portals-with-speakeasy-components Source: https://speakeasy.com/blog/dynamic-dev-portals-with-speakeasy-components ## New Features - **React-native Request Viewer** - Enable your users to troubleshoot broken API implementations on their own – dramatically reducing support costs and mean time-to-resolution. Just embed our request viewer in your developer portal or any customer facing app. The embed is fully featured: users can **filter requests (time, status code, endpoint, etc.), replay them immediately in-app, or share as a permalink** with your customer support team (no more emailed/slacked logs!). [Check out our NPM package here](https://www.npmjs.com/package/@speakeasy-api/webapp). [Start Here: Speakeasy API Platform and Dev portal Embeds - Watch Video](https://www.loom.com/share/bac492a4bbf34d62b7d241f87376986a) - **Python support for client-sdk generation** - Struggling to use the buggy open source openapi-generator libraries? We've added support for an idiomatic Python3 client SDK generated straight from your OpenAPI spec. Reach out if you're interested to try it out. It will soon be freely available through a standalone CLI. - **Run Speakeasy locally** - Start testing Speakeasy ASAP. Our latest release includes docker compose support, so you can easily run Speakeasy locally. ## Smaller Changes - **New API Dashboard UI - Our API dashboard has a fresh coat of paint. Overviews of every API are now represented by horizontal cards with all the relevant key stats and labels.** [**Login and check it out**](https://app.speakeasy.com/)**!** # ... Source: https://speakeasy.com/blog/e2e-testing-arazzo import { Callout, Table } from "@/mdx/components"; We've previously written about [the importance of building contract & integration tests](/post/contract-testing-with-openapi) to comprehensively cover your API's endpoints, but there's still a missing piece to the puzzle. Real users don't consume your API one endpoint at a time - they implement complex workflows that chain multiple API calls together. That's why reliable **end-to-end API tests** are an important component of the testing puzzle. For your APIs most common workflows, you need to ensure that the entire process works as expected, not just the individual parts. In this tutorial, we'll build a test generator that turns Arazzo specifications into executable end-to-end tests. You'll learn how to: - Generate tests that mirror real user interactions with your API - Keep tests maintainable, even as your API evolves - Validate complex workflows across multiple API calls - Catch integration issues before they reach production We'll use a simple "Build-a-bot" API as our example, but the principles and code you'll learn apply to any REST API. ## Arazzo? What & Why Arazzo is a specification that describes how API calls should be sequenced to achieve specific outcomes. Think of it as OpenAPI for workflows - while OpenAPI describes what your API can do, Arazzo describes how to use it effectively. Arazzo was designed to bridge the gap between API reference documentation and real-world usage patterns. Fortunately for us, it also makes a perfect fit for generating end-to-end test suites that validate complete user workflows rather than isolated endpoints. By combining these specifications, we can generate tests that validate not just the correctness of individual endpoints, but the entire user journey. Arazzo roughly translates to "tapestry" in Italian. Get it? A tapestry of API calls "woven" together to create a complete user experience. We're still undecided about how to pronounce it, though. The leading candidates are "ah-RAT-so" (like fatso) and "ah-RAHT-zoh" (almost like pizza, but with a rat). There is a minor faction pushing for "ah-razzo" as in razzle-dazzle. We'll let you decide. Let's look at a simplified (and mostly invalid) illustrative example. Imagine a typical e-commerce API workflow: ```yaml filename="arazzo.yaml" arazzo: 1.0.0 workflowId: purchaseProduct sourceDescriptions: - url: ./openapi.yaml steps: - stepId: authenticate operationId: loginUser # post login details # response contains auth token # if successful, go to checkInventory - stepId: checkInventory operationId: getProductsStock # with auth token from previous step: # get stock levels of multiple products # response contains product IDs and stock levels # if stock levels are sufficient, go to createOrder - stepId: createOrder operationId: submitOrder # with auth token from first step # and product IDs and quantities from previous step # post an order that is valid based on stock levels # response contains order ID # if successful, go to getOrder ``` Arazzo allows us to define these workflows, and specify how each step should handle success and failure conditions, as well as how to pass data between steps and even between workflows. ## From specification to implementation The example above illustrates the concept, but let's dive into a working implementation. We'll use a simplified but functional example that you can download and run yourself. Our demo implements a subset of the Arazzo specification, focusing on the most immediately valuable features for E2E testing. We'll use the example of an API called Build-a-bot, which allows users to create and manage their own robots. You can substitute this with your own OpenAPI document, or use the Build-a-bot API to follow along. If you want a deep dive on the Arazzo specification, check out the [Arazzo specification docs](/openapi/arazzo). ## Setting up the development environment First, clone the demo repository: ```bash git clone https://github.com/speakeasy-api/e2e-testing-arazzo.git cd e2e-testing-arazzo ``` You'll need [Deno v2](https://docs.deno.com/runtime/) installed. On macOS and Linux, you can install Deno using the following command: ```bash curl -fsSL https://deno.land/install.sh | sh ``` The repository contains: - A simple API server built with [@oak/acorn](https://oakserver.org/acorn) that serves as the Build-a-bot API (in `packages/server/server.ts`) - An Arazzo specification file (`arazzo.yaml`) - An OpenAPI specification file (`openapi.yaml`) - The test generator implementation (`packages/arazzo-test-gen/generator.ts`) - Generated E2E tests (`tests/generated.test.ts`) - An SDK created by Speakeasy to interact with the Build-a-bot API (`packages/sdk`) ### Running the Demo To run the demo, start the API server: ```bash deno task server ``` Deno will install the server's dependencies, then start the server on `http://localhost:8080`. You can test the server by visiting `http://localhost:8080/v1/robots`, which should return a `401 Unauthorized` error: ```json { "status": 401, "error": "Unauthorized", "message": "Header x-api-key is required" } ``` Next, in a new terminal window, generate the E2E tests: ```bash deno task dev ``` After installing dependencies, this command will generate the E2E tests in `tests/generated.test.ts` and watch for changes to the Arazzo specification file. You can run the tests in a new terminal window: ```bash deno task test ``` This command will run the generated tests against the API server: ```txt > deno task test Task test deno test --allow-read --allow-net --allow-env --unstable tests/ running 1 test from ./tests/generated.test.ts Create, assemble, and activate a new robot ... Create a new robot design session ... ok (134ms) Add parts to the robot ... ok (2ms) Assemble the robot ... ok (1ms) Configure robot features ... ok (2ms) Activate the robot ... ok (3ms) Get the robot details ... ok (1ms) Create, assemble, and activate a new robot ... ok (143ms) ok | 1 passed (6 steps) | 0 failed (147ms) ``` Beautiful, everything works! Let's see how we got here. ## Building an Arazzo test generator Let's start with the overall structure of the test generator. ### Project structure The test generator is a Deno project that consists of several modules, each with a specific responsibility: - `generator.ts`: The main entry point that orchestrates the test generation process. It reads the Arazzo and OpenAPI specifications, validates their compatibility, and generates test cases. - `readArazzoYaml.ts` and `readOpenApiYaml.ts`: Handle parsing and validation of the Arazzo and OpenAPI specifications respectively. They ensure the specifications are well-formed and contain all required fields. - `expressionParser.ts`: A parser for runtime expressions like `$inputs.BUILD_A_BOT_API_KEY` and `$steps.createRobot.outputs.robotId`. These expressions are crucial for passing data between steps and accessing workflow inputs. - `successCriteria.ts`: Processes the success criteria for each step, including status code validation, regex patterns, direct comparisons, and JSONPath expressions. - `generateTestCase.ts`: Takes the parsed workflow and generates the actual test code, including setup, execution, and validation for each step. - `security.ts`: Handles security-related aspects like API key authentication and other security schemes defined in the OpenAPI specification. - `utils.ts`: Contains utility functions for common operations like JSON pointer resolution and type checking. The project also includes a `runtime-expression` directory containing the grammar definition for runtime expressions: - `runtimeExpression.peggy`: A Peggy grammar file that defines the syntax for runtime expressions - `runtimeExpression.js`: The generated parser from the grammar - `runtimeExpression.d.ts`: TypeScript type definitions for the parser Let's dive deeper into each of these components to understand how they work together to generate effective E2E tests. ### Parsing until you parse out While our project says "test generator" on the tin, the bulk of our work will go into parsing different formats. To generate tests from an Arazzo document, we need to parse: 1. The Arazzo document 2. The OpenAPI document 3. Conditions in the Arazzo success criteria 4. Runtime expressions in the success criteria, outputs, and parameters 5. Regular expressions in the success criteria 6. JSONPath expressions in the success criteria 7. JSON pointers in the runtime expressions We won't cover all of these in detail, but we'll touch on each to get a sense of the complexity involved and the tools we use to manage it. ### Parsing the Arazzo specification The first step in our test generator is parsing the Arazzo specification in `readArazzoYaml.ts`. This module reads the Arazzo YAML file and should ideally validate its structure against the Arazzo specification. For our demo, we didn't implement full validation, instead parsing the YAML file into a JavaScript object. We then use TypeScript interfaces to define the expected structure of the Arazzo document: ```typescript export interface ArazzoDocument { arazzo: string; info: ArazzoInfo; sourceDescriptions: Array; workflows: Array; components: Record; } export interface ArazzoWorkflow { workflowId: string; description: string; inputs: { type: string; properties: Record; }; steps: Array; } export interface ArazzoStep { stepId: string; description: string; operationId: string; parameters?: Array; requestBody?: ArazzoRequestBody; successCriteria: Array; outputs?: Record; } ``` These TypeScript interfaces help with autocompletion, type checking, and documentation, making it easier to work with the parsed Arazzo document in the rest of our code. The real complexity comes in validating that the parsed document follows all the rules in the Arazzo specification. For example: - Each `workflowId` must be unique within the document - Each `stepId` must be unique within its workflow - An `operationId` must reference a valid operation in the OpenAPI document - Runtime expressions must follow the correct syntax - Success criteria must use valid JSONPath or regex patterns We don't validate all these rules in our demo, but in production, we'd use [Zod](https://zod.dev/) or [Valibot](https://github.com/fabian-hiller/valibot) to enforce these constraints at runtime and provide helpful error messages when the document is invalid. The OpenAPI team hasn't finalized the Arazzo specification's JSON Schema yet, but once they do, we can use it to validate the Arazzo document against the schema with tools like [Ajv](https://ajv.js.org/). Speakeasy also provides a [command-line interface](/docs/speakeasy-cli/getting-started) for linting Arazzo documents: ```bash # Expects arazzo.yaml in the current directory speakeasy lint arazzo ``` ### Parsing the OpenAPI specification The OpenAPI specification's path is gathered from the Arazzo document. In our test, we simply use the first `sourceDescription` to find the OpenAPI document path. But in a production generator, we'd need to handle multiple `sourceDescriptions` and ensure the OpenAPI document is accessible. We parse the OpenAPI document in `readOpenApiYaml.ts` and use TypeScript interfaces from the [`npm:openapi-types`](https://www.npmjs.com/package/openapi-types) package to define the expected structure of the OpenAPI document. We won't cover the OpenAPI parsing in detail, but it's similar to the Arazzo parsing: Read the YAML file, parse it into a JavaScript object, and type check it against TypeScript interfaces. For OpenAPI, writing a custom validator is more complex due to the specification's size and complexity. We recommend validating against the [official OpenAPI 3.1.1 JSON Schema](https://spec.openapis.org/oas/v3.1.1.html) using [Ajv](https://ajv.js.org/), or Speakeasy's own OpenAPI linter: ```bash speakeasy lint openapi --schema openapi.yaml ``` ### Parsing success criteria This is where things get interesting. Success criteria in Arazzo are a list of conditions that must be met for a step to be considered successful. Each criterion can be one of the following types: - `simple`: Selects a value with a runtime expression and compares it to an expected value - `jsonpath`: Selects a value using a JSONPath expression and compares it to an expected value - `regex`: Validates a value against a regular expression pattern - `xpath`: Selects a value using an XPath expression and compares it to an expected value, used for XML documents In our demo, we don't implement the `xpath` type, but we do cover the other three. Here's an example of a success criterion in the Arazzo document: ```yaml successCriteria: - condition: $statusCode == 201 - condition: /^[0-9A-F]{8}-[0-9A-F]{4}-[4][0-9A-F]{3}-[89AB][0-9A-F]{3}-[0-9A-F]{12}$/i context: $response.body#/robotId type: regex - condition: $response.body#/model == "humanoid" - condition: $response.body#/name == "MyFirstRobot" - condition: $response.body#/status == "designing" - context: $response.body#/links condition: $.length == 5 type: jsonpath ``` The `condition` field is required, and contains the expression to evaluate, while the `context` field specifies the part of the response to evaluate. The `type` field indicates the type of validation to perform. If no `type` is specified, the success criterion is treated as a `simple` comparison, where the `condition` is evaluated directly. ### Evaluating simple criteria Here's an example of how we parse a `simple` success criterion: ```yaml condition: $statusCode == 201 ``` We split this simple condition into: - Left-hand side: `$statusCode` - Runtime expression to evaluate - Operator: `==` - Comparison operator or assertion - Right-hand side: `201` - Expected value We'll evaluate the runtime expression `$statusCode` and compare it to the expected value `201`. If the comparison is true, the criterion passes; otherwise, it fails. Runtime expressions can also reference other variables, like `$inputs.BUILD_A_BOT_API_KEY` or `$steps.createRobot.outputs.robotId`, or fields in the response body, like `$response.body#/model`. We'll cover runtime expressions in more detail later. ### Evaluating JSONPath criteria For JSONPath criteria, we use the `jsonpath` type and a JSONPath expression to select a value from the response: ```yaml context: $response.body#/links condition: $.length == 5 type: jsonpath ``` Let's break down the JSONPath criterion: - `context`: `$response.body#/links` - Runtime expression to select the `links` array from the response body - `condition`: `$.length == 5` - JSONPath expression compared to an expected value - `type`: `jsonpath` - Indicates the criterion type We further need to break down the condition into: - Left-hand side: `$.length` - JSONPath expression to evaluate - Operator: `==` - Comparison operator - Right-hand side: `5` - Expected value We evaluate the JSONPath expression `$.length` and compare it to the expected value `5`. If the comparison is true, the criterion passes. ### Evaluating regex criteria For regex criteria, we use the `regex` type and a regular expression pattern to validate a value: ```yaml context: $response.body#/robotId condition: /^[0-9A-F]{8}-[0-9A-F]{4}-[4][0-9A-F]{3}-[89AB][0-9A-F]{3}-[0-9A-F]{12}$/i type: regex ``` Let's break down the regex criterion: - `context`: `$response.body#/robotId` - Runtime expression to select the `robotId` field from the response body - `condition`: `/^[0-9A-F]{8}-[0-9A-F]{4}-[4][0-9A-F]{3}-[89AB][0-9A-F]{3}-[0-9A-F]{12}$/i` - Regular expression pattern to validate the value as a UUID v4 - `type`: `regex` - Indicates the criterion type We evaluate the runtime expression `$response.body#/robotId` against the regular expression pattern. If the value matches the pattern, the criterion passes. In our implementation, we use TypeScript's `factory.createRegularExpressionLiteral` to create a regular expression literal from the pattern string. This ensures that the pattern is properly escaped and formatted as a valid JavaScript regular expression. The generated test code looks something like this: ```typescript assertMatch( response.body.robotId, new RegExp(/^[0-9A-F]{8}-[0-9A-F]{4}-[4][0-9A-F]{3}-[89AB][0-9A-F]{3}-[0-9A-F]{12}$/i), 'robotId should be a valid UUID v4' ); ``` This code uses Deno's built-in `assertMatch` function to validate that the `robotId` matches the UUID v4 pattern. If the value doesn't match, the test fails with a helpful error message. ### Parsing runtime expressions Runtime expressions are used throughout the Arazzo specification to reference variables, fields in the response body, or outputs from previous steps. They follow a specific syntax defined in the Arazzo specification as an ABNF (augmented Backus–Naur form) grammar. To parse runtime expressions, we use a parser generated from the ABNF grammar. In our demo, this is a two-step process. First, we use the [abnf](https://www.npmjs.com/package/abnf) npm package to generate a Peggy grammar file from the ABNF grammar: ```bash cd packages/arazzo-test-gen abnf_gen runtime-expression/runtimeExpression.abnf ``` This generates a `runtime-expression/runtimeExpression.peggy` file that defines the syntax for runtime expressions. We then use the [peggy](https://www.npmjs.com/package/peggy) npm package to generate a parser from the Peggy grammar: ```bash cd packages/arazzo-test-gen peggy --dts --output runtime-expression/runtimeExpression.js --format es runtime-expression/runtimeExpression.peggy ``` This generates a `runtime-expression/runtimeExpression.js` file that contains the parser for runtime expressions. We also generate TypeScript type definitions in `runtime-expression/runtimeExpression.d.ts`. The parser reads a runtime expression like `$response.body#/robotId` and breaks it down into tokens. We then evaluate the tokens to resolve the expression at runtime. ### Evaluating runtime expressions Once we've parsed a runtime expression, we need to evaluate it to get the value it references. For example, given the expression `$response.body#/robotId`, we need to extract the `robotId` field from the response body. The `evaluateRuntimeExpression` function in `utils.ts` handles this evaluation. Here's an example of how it works: ```typescript switch (root) { case "$statusCode": { // Handle $statusCode expressions result = factory.createPropertyAccessExpression( factory.createIdentifier("response"), factory.createIdentifier("status"), ); break; } case "$response.": { // Handle $request and $response expressions const data = factory.createIdentifier("data"); // use parseRef to handle everything after $response.body const pointer = parsePointer(expression.slice(15)); result = pointer.length > 0 ? factory.createPropertyAccessExpression(data, pointer.join(".")) : data; break; } // Handle other cases ... } ``` Here, we handle two types of runtime expressions: `$statusCode` and `$response.body`. We extract the `status` field from the `response` object for `$statusCode`, and the `body` object from the `response` object for `$response.body`. We use the TypeScript compiler API to generate an abstract syntax tree (AST) that represents the expression. This AST is then printed to a string and saved as a source file that Deno can execute. ### Supported runtime expressions In our demo, we support a limited set of runtime expressions: - `$statusCode`: The HTTP status code of the response - `$steps.stepId.outputs.field`: The output of a previous step - `$response.body#/path/to/field`: A field in the response body selected by a JSON pointer Arazzo supports many more runtime expressions, for example:
### Parsing regular expressions Regular expressions in Arazzo are used to validate string values against patterns. They're particularly useful for validating IDs, dates, and other structured strings. In our implementation, we handle regex patterns in the `parseRegexCondition` function: ```typescript function parseRegexCondition( condition: string, usedAssertions: Set, context: string, ): Expression { usedAssertions.add("assertMatch"); return factory.createCallExpression( factory.createIdentifier("assertMatch"), undefined, [ evaluateRuntimeExpression(context), factory.createNewExpression( factory.createIdentifier("RegExp"), undefined, [factory.createRegularExpressionLiteral(condition)], ), factory.createStringLiteral(condition), ], ); } ``` This function takes three parameters: - `condition`: The regex pattern to match against - `usedAssertions`: A set to track which assertion functions we've used - `context`: The runtime expression that selects the value to validate The function generates code that: 1. Evaluates the context expression to get the value to validate 2. Creates a new RegExp object from the pattern 3. Uses Deno's `assertMatch` function to validate the value against the pattern The generated code looks like this: ```typescript assertMatch( response.body.robotId, new RegExp(/^[0-9A-F]{8}-[0-9A-F]{4}-[4][0-9A-F]{3}-[89AB][0-9A-F]{3}-[0-9A-F]{12}$/i), 'robotId should be a valid UUID v4' ); ``` This approach has several advantages: - It preserves the original pattern's flags (like `i` for case-insensitive matching). - It provides clear error messages when validation fails. - It integrates well with Deno's testing framework. In a production implementation, we'd want to add: - Validation of the regex pattern syntax - Support for named capture groups - Error handling for malformed patterns - Performance optimizations like pattern caching But for our demo, this simple implementation is sufficient to show how regex validation works in Arazzo. ### Parsing JSONPath expressions JSONPath expressions are a powerful way to query JSON data. In Arazzo, we use them in success criteria to select objects or values from complex response structures. While JSON Pointer (which we'll cover next) is great for accessing specific values, JSONPath shines when you need to: - Validate arrays (for example, checking array length) - Filter elements (for example, finding items matching a condition) - Access deeply nested data with wildcards - Aggregate values (for example, counting matches) Here's how our test generator handles JSONPath expressions: ```typescript function parseJsonPathExpression(path: string, context: string): Expression { return factory.createCallExpression( factory.createIdentifier("JSONPath"), undefined, [ factory.createObjectLiteralExpression( [ factory.createPropertyAssignment( factory.createIdentifier("wrap"), factory.createFalse(), ), factory.createPropertyAssignment( factory.createIdentifier("path"), factory.createStringLiteral(path), ), factory.createPropertyAssignment( factory.createIdentifier("json"), evaluateRuntimeExpression(context), ), ], true, ), ], ); } ``` This function generates code that evaluates a JSONPath expression against a context object (usually the response body). For example, given this success criterion: ```yaml successCriteria: - context: $response.body#/links condition: $.length == 5 type: jsonpath ``` Our generator creates a test that: 1. Extracts the `links` array from the response body using a JSON pointer 2. Evaluates the JSONPath expression `$.length` against this array 3. Compares the result to the expected value `5` The generated test code looks something like this: ```typescript assertEquals( JSONPath({ wrap: false, path: "$.length", json: response.body.links }), 5, "links array should contain exactly 5 elements" ); ``` JSONPath is particularly useful for validating: - Array operations: `$.length`, `$[0]`, `$[(@.length-1)]` - Deep traversal: `$..name` (all name properties at any depth) - Filtering: `$[?(@.status=="active")]` (elements where status is `active`) - Wildcards: `$.*.name` (name property of all immediate children) A few things to keep in mind when using JSONPath: 1. JSONPath isn't well standardized, so different implementations vary widely. Arazzo makes provisions for this by allowing us to specify the JSONPath version in the test specification. 2. Even though we can specify a version, we still need to be cautious when using advanced features. Some features might not be supported by the chosen JSONPath library. 3. Check the [JSONPath comparison](https://cburgmer.github.io/json-path-comparison/) page to see how different libraries handle various features, and decide which features are safe to use. ### Parsing JSON Pointers While JSONPath is great for complex queries, [JSON Pointer (RFC 6901)](https://datatracker.ietf.org/doc/html/rfc6901) is perfect for directly accessing specific values in a JSON document. In Arazzo, we use JSON Pointers in runtime expressions to extract values from responses and pass them to subsequent steps. Here's how our test generator handles JSON Pointers: ```typescript function evaluateRuntimeExpression(expression: string): Expression { // ... case "$response.": { const data = factory.createIdentifier("data"); // Parse everything after $response.body const pointer = parsePointer(expression.slice(15)); result = pointer.length > 0 ? factory.createPropertyAccessExpression(data, pointer.join(".")) : data; break; } // ... } ``` This function parses runtime expressions that use JSON Pointers. For example, given this output definition: ```yaml outputs: robotId: $response.body#/robotId ``` Our generator creates code that: 1. Takes the part after `#` as the JSON Pointer (`/robotId`) 2. Converts the pointer segments into property access expressions 3. Generates code to extract the value The generated test code looks something like this: ```typescript // During test setup const context = {}; // ... // In the first test const data = response.json(); // Generated because of outputs: { robotId: $response.body#/robotId } in the Arazzo document // highlight-next-line context["createRobot.outputs.robotId"] = data.robotId; // ... // In a subsequent test const robotId = context["createRobot.outputs.robotId"]; const response = await fetch(`${serverUrl}/v1/robots/${robotId}/assemble`, { // ... }); ``` ## Generating end-to-end tests Now that we understand how to parse Arazzo documents, let's look at how we generate executable tests from them. Our generator creates type-safe test code using TypeScript's factory methods rather than string templates, providing better error detection and maintainability. ### Test structure The generator creates a test suite for each workflow in the Arazzo document. Each step in the workflow becomes a test case that executes sequentially. Let's explore the structure of a generated test case. We start by setting up a test suite for the workflow, using the Arazzo workflow `description` as the suite name. ```typescript filename="generated.test.ts" focus=9 !from ./generated.test.ts.txt ``` Next we define the `serverUrl`, `apiKey`, and `context` variables. The `serverUrl` points to the API server. We use the `servers` list in the OpenAPI document to determine the server URL. We also set up the `apiKey` for authentication. In our demo, we use a hardcoded API key, but in a real-world scenario, we'd likely get this after authenticating with the API. We'll use the `context` object to store values extracted from the response body for use in subsequent steps. ```typescript filename="generated.test.ts" focus=10:12 !from ./generated.test.ts.txt ``` For each step in the workflow, we generate a test case that executes the step and validates the success criteria. Our first step is to create a new robot design session. ```typescript filename="generated.test.ts" focus=13 !from ./generated.test.ts.txt ``` The HTTP method and path are extracted from the OpenAPI document using the `operationId` from the Arazzo step. ```typescript filename="generated.test.ts" focus=14:15 !from ./generated.test.ts.txt ``` We set up the request headers, including the `x-api-key` header for authentication. ```typescript filename="generated.test.ts" focus=16:19 !from ./generated.test.ts.txt ``` The request body is set up using the `requestBody` object from the Arazzo step. ```typescript filename="generated.test.ts" focus=20 !from ./generated.test.ts.txt ``` We extract the response body as JSON. ```typescript filename="generated.test.ts" focus=22 !from ./generated.test.ts.txt ``` We assert the success criteria for the step. ```typescript filename="generated.test.ts" focus=23:50 !from ./generated.test.ts.txt ``` Finally, we extract the outputs from the step and store them in the `context` object for use in subsequent steps. ```typescript filename="generated.test.ts" focus=51 !from ./generated.test.ts.txt ``` This structure repeats for each step in the workflow, creating a series of test cases that execute the workflow sequentially. The generated tests validate the API's behavior at each step, ensuring that the workflow progresses correctly. ## Future development and improvements Our generated tests are a good start, but they might not be truly end-to-end if we don't consider the interfaces our users interact with to access the API. ### Testing with SDKs In our demo, we use the `fetch` API to interact with the Build-a-bot API. While this is a common approach, it's not always the most user-friendly. Developers often prefer SDKs that provide a more idiomatic interface to the API. To make our tests more end-to-end, we could use the SDK Speakeasy created from the OpenAPI document to interact with the API. Since the SDK is generated from the OpenAPI document, with names and methods derived from the API's tags and operation IDs, we could use Arazzo to validate the SDK's behavior against the API's capabilities. For example, we could: 1. Get the `operationId` from the Arazzo step and derive the corresponding SDK method import. 2. Call the SDK method with the required parameters. 3. Validate the response against the success criteria. 4. Extract the outputs from the response and store them in the `context` object. 5. Repeat for each step in the workflow. This approach would provide a more realistic end-to-end test, validating the SDK's behavior against the API's capabilities. ### Handling authentication In our demo, we use a hard-coded API key for authentication. In a real-world scenario, we'd likely need to authenticate with the API to get a valid API key. OpenAPI also supports more advanced authentication schemes like OAuth 2.0, JWT, and API key in headers, query parameters, or cookies. Our test generator should handle these schemes to ensure the tests are realistic and cover all authentication scenarios. Arazzo can point to the security schemes in the OpenAPI document, allowing us to extract the required authentication parameters and set them up in the test suite. ### Hardening the parsers against vulnerabilities Our parsers are simple and work well for the demo, but they lack robust error handling and edge case coverage. For example, JSONPath-plus, the library we use for JSONPath, recently fixed a remote code execution vulnerability. We should ensure our parser is up to date and secure against similar vulnerabilities, or limit the JSONPath features we support to reduce the attack surface. This applies to parsers in general, and the risk is even higher when parsing user input and generating code from it. Deno provides some protection by limiting access to the filesystem and network by default, but the nature of API testing means we need to access the network and read files. ## Where to next? The Arazzo specification, although released as v1.0.0, is in active development. The OpenAPI team is working on a JSON Schema for Arazzo, which will provide a formal definition of the specification's structure and constraints. We found the specification slightly ambiguous in places, but the team is [active on GitHub](https://github.com/OAI/Arazzo-Specification/issues) and open to feedback and contributions. If you're interested in API testing, Arazzo is a great project to get involved with. At Speakeasy, we're building tools to make API testing easier and more effective. Our TypeScript, Python, and Go SDK generators can already generate tests from OpenAPI documents, and we're working on integrating Arazzo support. Our CLI can already lint Arazzo documents, and we'll have more to share soon. We're excited to see how Arazzo evolves and how it can help developers build robust, end-to-end tests for their APIs. # early-access-for-universal-typescript-sdk-and-sdk-docs Source: https://speakeasy.com/blog/early-access-for-universal-typescript-sdk-and-sdk-docs Welcome to another edition of the Speakeasy Changelog. In this issue, we will give you a sneak peek into products rolling hot off the press--our latest Typescript release as well as SDK Documentation. Sound good? Ok, let's go! 🚀 ## (Early Access) Universal Typescript SDK We're super excited to announce early access to our newest Typescript SDK. This SDK is designed to be a truly universal interface created to run on servers and in the browser, and in any Javascript runtime! Powered by [fetch](https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API) and [Zod](https://zod.dev/), it has a minimal code footprint while providing a rich toolkit for one of the fastest-growing and evolving language communities. Other goodies include: - Built-in runtime validation for types, - Support for union types and Bigints, - Native streaming upload and download support. Got an AI product that streams megabytes of data per request and response? No problem, we've got your users covered. Check out that slick interface: ```typescript import { DrinkType, Speakeasybar } from "speakeasy"; async function run() { const sdk = new Speakeasybar({ apiKey: "my_key", }); const drinkType = DrinkType.Spirit; const res = await sdk.drinks.listDrinks(drinkType); if (res?.statusCode !== 200) { throw new Error("Unexpected status code: " + res?.statusCode || "-"); } // handle response } run(); ``` Read more about it [here](../post/introducing-tsv2)! Or join our [Slack](https://go.speakeasy.com/slack) for early access! ## (Early Access) SDK Documentation We're also excited to announce SDK Docs into early access! We've always believed that great developer experience starts with type safe experiences and rapid time to 200s. We believe this should extend to your documentation as well. That's why we're releasing support for generating great SDK docs straight from your OpenAPI spec. Get a fully working NextJS powered app with embedded usage snippets just by running `speakeasy generate docs .... `! Or plug into our automation and get a working docs site that stays up to date with every API and SDK change. ![API docs with SDKs featured](/assets/changelog/assets/sdk-docs.jpg) We've built SDK docs to provide a language-specific guide that gets your users to a fully working integration with just a single copy-paste. Let's face it: 3-pane API references are out of date! It's time for something new. Join our [Slack](https://go.speakeasy.com/slack) to get early access to SDK Docs! ## New OpenAPI Guides: We have two new posts in our OpenAPI tips series covering best practices when working with tags and servers in your OpenAPI document. These simple concepts, when used effectively, can greatly improve the developer experience for your end users. - [Working with Servers](../post/openapi-servers): The servers block can be a great way to provide your users with convenient access to production, staging, sandbox and other environments you might have. Maybe your API supports tenancy? No problem! You can template out your server endpoints with variables. - [Working with Tags](../post/tags-best-practices-in-openapi): Tags can be your best friend when organising your API resources into logical groups for your users. `sdk.payments.list` ? That's nice and easy to use :) ## 🚢 Improvements and Bug Fixes 🐛 #### [Speakeasy v1.125.1](https://github.com/speakeasy-api/speakeasy/releases/tag/v1.125.1) 🚢 Improved preferred usage examples selection\ 🚢 Server selection section automatically added to Readmes\ 🚢 Complex serialization of deep objects in additionalProperties ### Terraform 🐛 Importing of external array of enums ### Python 🐛 Unused imports in Python for errors\ 🐛 Relaxed urllib3 dependency in Python to support boto3 ### Java 🚢 Nullable support\ 🚢 Header capture in operation responses ### C# 🚢 Support for server variables\ 🐛 Class names conflicting with namespaces and SDKs in C#/Unity # easy-validation-for-openapi Source: https://speakeasy.com/blog/easy-validation-for-openapi New Year means new product shipping! When you encounter an error while troubleshooting, you want to know **where** the error occurred and **what** caused it. Anyone who's tried to do code generation from an OpenAPI spec is unfortunately very familiar with the anguish of hunting through a thousand-line yaml file to track down opaque errors without line numbers to guide you. With the latest release of our CLI, that should hopefully be a problem of the past. We've baked in an OpenAPI validator that will help you optimize your spec for code generation workflows and make troubleshooting any issues a breeze. See it in action below. ### **New Features** **OpenAPI Validation** - OpenAPI validation is baked into [our SDK generator](/docs/sdks/create-client-sdks/), but can also be used on its own: `speakeasy validate openapi -s openapi.yaml`. The validator provides you with both `warnings`: implementation that is bad practice, but not incorrect and `errors`: implementation that violates the OpenAPI spec. The best part is that the warnings and errors returned are actionable. The error messages are human-readable, and include a line number to make it easy to track down: ```yaml Error: validation error: [line 12] validate-servers - Server URL is not valid: no hostname or path provided ``` To try it out yourself, download the CLI: ```bash ***brew install speakeasy-api/tap/speakeasy*** ``` ```bash ***speakeasy validate openapi -s openapi.yaml*** ``` # easytemplate-release Source: https://speakeasy.com/blog/easytemplate-release At Speakeasy, we work in a variety of languages, but most of our backend is written in Go, specifically for its no nonsense outlook on code quality and long term maintainability. Without Go's vibrant OSS community, we wouldn't have been able to build the product we have today, which is why we're very excited to have the opportunity to contribute back to the community. [**Check it out**](https://github.com/speakeasy-api/easytemplate) ## What is EasyTemplate? [**easytemplate**](https://github.com/speakeasy-api/easytemplate) is Go's [text/template](https://pkg.go.dev/text/template) with super powers. It is a templating engine that allows you to use Go's [text/template](https://pkg.go.dev/text/template) syntax, but with the ability to use JavaScript or Typescript snippets to manipulate data, control templating and run more complex logic while templating. [**easytemplate**](https://github.com/speakeasy-api/easytemplate) powers Speakeasy's [SDK Generation](/post/client-sdks-as-a-service/) product and is used by thousands of developers to generate SDKs for their APIs. The module includes a number of features on top of the standard [text/template](https://pkg.go.dev/text/template) package, including: - [Support for JavaScript snippets in templates](https://github.com/speakeasy-api/easytemplate#using-javascript). - ES5 Support provided by [goja](https://github.com/dop251/goja). - Built-in support for [underscore.js](http://underscorejs.org/) - Import JavaScripts scripts from other files and inline JavaScript snippets - Use JavaScript or Typescript - Modify the templating context from within JavaScript. - [Controlling the flow of templating within the engine](https://github.com/speakeasy-api/easytemplate#controlling-the-flow-of-templating). - [Inject Go functions into the JavaScript context](https://github.com/speakeasy-api/easytemplate#registering-js-functions-from-go), in addition to [Go functions into the templates](https://github.com/speakeasy-api/easytemplate#registering-templating-functions). - [Inject JS functions into the template context.](https://github.com/speakeasy-api/easytemplate#registering-js-templating-functions) ## Why'd we build it? Speakeasy needed a way of templating complex hierarchies of templates that all relied on each other and the content they contained (like for when you generate SDKs from API Specs). By building a templating engine that allows more complex logic to be run at templating time via JS and allowing templates to template other templates, we unlock the ability to tailor templates to our needs based on the target output. This allows us to decouple templating from our core binary, allowing new templates to be provided at runtime (think plugins) without the core go code/binary needing to know what templates there are, what data they need, enabling the templating to call itself on a dynamic set of files. We chose JS/TS as the language for the embedded scripting because of its ubiquity, and ease of learning. It also has a thriving ecosystem of data and string manipulation modules which provide additional super powers to your templates. ## Basic Example `main.go` ```go package main import ( "fmt" "log" "os" "github.com/speakeasy-api/easytemplate" ) func main() { // Create a new easytemplate engine. engine := easytemplate.New() // Start the engine from a javascript entrypoint. err := engine.RunScript("main.js", data) if err != nil { log.Fatal(err) } } ``` `main.js` ```js // From our main entrypoint, we can render a template file, the last argument is the data to pass to the template. templateFile("tmpl.stmpl", "out.txt", { name: "John" }); ``` `tmpl.stmpl` In the below template we are using the `name` variable from the data we passed to the template from main.js. We then also have an embedded JavaScript block that both renders output (the sjs block is replaced in the final output by any rendered text or just removed if nothing is rendered) and sets up additional data available to the template that it then uses to render another template inline. ```go Hello {{ .Local.name }}! ```sjs console.log("Hello from JavaScript!"); // Logs message to stdout useful for debugging. render("This text is rendered from JavaScript!"); context.LocalComputed.SomeComputedText = "This text is computed from JavaScript!"; sjs``` {{ templateString "tmpl2.stmpl" .LocalComputed }} ``` `tmpl2.stmpl` ```go And then we are showing some computed text from JavaScript: {{ .SomeComputedText }} ``` The rendered file `out.txt` ```text Hello John! This text is rendered from JavaScript! And then we are showing some computed text from JavaScript: This text is computed from JavaScript! ``` ## How should you use it? `easytemplate` allows you to control templating directly from scripts or other templates which among other things, allows you to: - Break templates into smaller, more manageable templates and reuse them, while also including them within one another without the need for your Go code to know about them or control the flow of templating them. - Provide templates and scripts at runtime allowing pluggable templating for your application. - Separate your templates and scripts from your Go code, allowing you to easily update them without having to recompile your application and keeping concerns separate. We can't wait to see what the Go community uses EasyTemplate for! # enforcing-api-consistency Source: https://speakeasy.com/blog/enforcing-api-consistency import { Callout } from "@/mdx/components";

If you're looking for a more comprehensive guide to API design, you can read our REST API Design Guide.

API style guides often make developers' work more difficult, which leads to interface drift, which in turn affects real users. This is easy to resolve within a [two-pizza team](https://martinfowler.com/bliki/TwoPizzaTeam.html) using code review, pair programming, and the collective identity that results from time spent in the trenches together. Small teams have enough rapport and opportunities for interaction to resolve API design issues, update their style guides, and reach consensus without much friction. Sure, it isn't always as rose-colored as that, but compared to reaching consensus *between* teams, it's a walk in the park. Friction between teams becomes a giant burden to organizations that have multiple teams working on a single API, and even more so when teams are working on multiple adjacent APIs. The common response is to create an "API governance team" or "architecture review board" - a group of senior engineers tasked with maintaining consistency across teams. It is painful just typing those phrases out. The idea is well-intentioned, but the execution is often disastrous: * Teams wait weeks for reviews, slowing down development. * Reviewers become bottlenecks, causing resentment. * Standards become rigid and divorced from real-world needs. * Teams find creative ways to bypass the process entirely. This compounds until there is enough drift that consistency becomes an unattainable dream. What's needed instead is a way to enforce consistency that works *with* teams rather than against them. This means moving from manual processes to automated checks; from subjective reviews to objective criteria. It means building tools that help developers stay consistent, and providing a clear path to resolution when they don't. ## What do we mean by consistency? At its core, consistency means following the [principle of least astonishment](https://en.wikipedia.org/wiki/Principle_of_least_astonishment): APIs should behave in ways that minimize surprises for everyone who interacts with them. Here's what this looks like in practice: ### Within your interfaces The most fundamental form of consistency is within individual APIs. When developers interact with different parts of a single API, they should be able to apply what they've learned from one endpoint to other endpoints. Here are some examples of internal consistency: 1. **Use the same naming patterns across all endpoints:** For example, if you use `snake_case` for field names in one place, you shouldn't switch to `camelCase` in another. 2. **Ensure request and response objects are uniform:** A reasonable assumption is that if an e-commerce API has a `city` field in the `orders` endpoint, it should have the same field in the `returns` endpoint. Replacing `city` with `town` in the `returns` endpoint would be inconsistent and confusing. 3. **Follow the same error-handling patterns throughout:** All endpoints should return errors in a consistent format, with appropriate status codes and messages. If a `GET` request to a resource that doesn't exist returns a `404 Not Found` status code, a `POST` request to the same resource should return the same status code. 4. **Keep authentication flows predictable:** If a user needs to provide a token in the `Authorization` header for one endpoint, they should reasonably expect to use the same header for all other endpoints that require authentication, rather than a query parameter or a cookie. This internal consistency allows developers to build accurate mental models of how your API works. ### Across your organization The next level of consistency extends across your organization. This means that every team building APIs should follow the same conventions. This includes: 1. **Shared authentication mechanisms across services:** If one team uses OAuth2 for authentication, all teams should use OAuth2. This ensures that developers don't need to learn new authentication mechanisms when switching between services. 2. **Common error handling across services:** When a service is down, every API should return a `503` status code with the same error structure, not a mix of different formats and codes for different services. 3. **Unified naming conventions:** If your user service uses `/v1/users/{id}` as a pattern, your order service shouldn't use `/api/2.0/orders/{orderId}`. This kind of inconsistency means developers have to remember different patterns for different services. 4. **Standard versioning approaches:** Allowing some teams to use URL versioning (`/v1/resource`) while others use accept headers (`Accept: application/vnd.company.resource.v1+json`) creates unnecessary complexity. 5. **Consistent rate-limiting implementations:** Rate limits should use the same headers and behavior across services. If one service uses `X-RateLimit-Remaining` while another uses `RateLimit-Remaining`, developers need to handle both cases. This is where the most friction occurs. Teams have different priorities, different constraints, and different preferences. It's easy for standards to drift when there's no shared understanding of why they exist. We'll explore how to address this in more detail later. ### With your domain Your API should make sense to developers who work in your industry: 1. **Use familiar field names:** A payment API using `amount` and `currency` will feel more natural than `monetary_value` and `money_type`. 2. **Follow standard workflows:** An e-commerce API should follow common patterns for checkout flows that developers will recognize from other platforms. 3. **Support expected features:** If every other API in your space supports bulk operations, your API probably should too. This requires in-depth domain knowledge and an understanding of what developers expect from APIs in your industry. It's the hardest form of consistency to enforce, but it's also one of the most valuable. ### With HTTP standards Most developers have expectations about how HTTP works. For example: 1. **GET requests should be safe and idempotent:** They shouldn't change data. A `GET` request to `/users/123` should never delete the user or have any other side effects. 2. **POST is for creation:** Only `POST` should be used to create new resources, not `PUT` or `GET`. 3. **Status codes should follow conventions:** Use `201` for successful creation, not `200` with a `"created"` string in the body. 4. **Cache headers should work:** If you say something can be cached for an hour, it should be safe to cache for an hour. The goal isn't to perfect adherence to HTTP specifications but to meet developers' reasonable expectations about how HTTP works. We discuss this balance in more detail in our article, [Designing your API: Find the RESTful sweet spot](https://www.speakeasy.com/post/api-design). ## Focusing on what matters most It is easy to get stuck in a tar pit while trying to deliberate and enforce every possible form of consistency. Bikeshedding and navel-gazing over insignificant details can lead to a loss of focus on what really matters. Here's how to prioritize: ### Non-negotiables Some forms of consistency are so important that they should be enforced in all but the most exceptional cases. These are the things that will cause the most confusion and frustration if they're inconsistent: 1. **Authentication:** Security patterns need to be predictable and well-understood. Never roll your own authentication scheme, and always use the same mechanism across services. 2. **Error handling:** Consistent error responses are useful for developers to understand what went wrong. If every service returns a different error format, developers will waste time debugging. 3. **HTTP methods:** Stick to the standard HTTP methods and their meanings. This is one of the most fundamental forms of consistency in REST APIs and has been well-established for decades. 4. **HTTP status codes:** Status codes have well-defined meanings. If a resource isn't found, return a `404`. If a user is unauthorized, return a `401`. Don't reinvent the wheel here, and never return a `200` status code for an error. 5. **URL structure:** Predictable URLs make it easier for developers to navigate your API and discover endpoints. ### When to be flexible Apart from these non-negotiables, most other forms of consistency can be more flexible for the right reasons. Here are some examples: 1. **Performance:** Bulk operations may return stripped-down resources to improve performance, while individual operations return full resources. This is a reasonable trade-off that can be explained in your documentation. 2. **Naming conventions:** If a team has a good reason for using a different naming convention, it's not worth enforcing consistency for its own sake. The goal is to make your API easier to use, not to make it uniform at all costs. For example, if the `users` resource is called a `debtor` in a financial API, that's fine as long as it's well-documented. 3. **Rate limiting:** Different services may have different rate limits based on their usage patterns. It's okay for these to vary as long as they're documented clearly. In all three examples, documentation saves the day. If developers understand *why* things are inconsistent, they can work around it. If they don't understand your reasons, they may lose confidence in your API, or worse, think they made a mistake elsewhere and go bug-hunting. Explaining the reasons behind inconsistencies is often more important than enforcing consistency for its own sake. ## Consistency isn't the same for everyone Our definition of consistency is based on the principle of least astonishment, but what's surprising in one context may be expected in another. Start by understanding your developers' expectations and work from there. Different groups of developers bring different expectations. For example, developers in the financial industry may expect idempotency keys on all write operations, while developers in the gaming industry may not. This is a balancing act. Some industries have become accustomed to certain patterns that may not be best practice. For example, the financial industry's reliance on SOAP APIs with complex XML payloads is a well-established pattern, but it's not the most developer-friendly approach. In this case, consistency with the industry may not be the best choice. This may be one of the reasons for Stripe's success - they take a developer-first approach to payments, rather than following the industry standard. Once you understand the expectations of your developers, you can start prioritizing consistency based on what really matters in your context. ## How to enforce consistency With a clear understanding of what consistency means for your API, you can start enforcing it. ### Use OpenAPI An OpenAPI document should be the source of truth for your API. If your API framework doesn't generate OpenAPI documents, consider switching to one that does or adding a tool to generate them. Ideally, your OpenAPI document should either be generated automatically from your codebase or act as a contract that your codebase adheres to. Specifying your API in OpenAPI allows your teams to discuss and agree on standards without implementing them. It also allows you to generate documentation, client libraries, and server stubs automatically. In the context of consistency, OpenAPI enables you to automate checks for internal consistency. ### Automated enforcement Start by automating everything that can be objectively verified: #### Automate OpenAPI validation Your API definitions should be valid according to the OpenAPI Specification. This is table stakes and should be enforced through CI/CD pipelines. #### Automate linting Use tools like [Spectral](https://github.com/stoplightio/spectral) to enforce style conventions. Create a ruleset that codifies your organization's standards. This can include naming conventions, error-handling patterns, and more. Spectral has built-in rules for common patterns, but you can also write custom rules to enforce your organization's specific standards. For example, you could enforce `kebab-case` for all paths: ```yaml rules: paths-kebab-case: description: Paths should be kebab-case. message: "{{property}} should be kebab-case (lower-case and separated with hyphens)" severity: warn given: $.paths[*]~ then: function: pattern functionOptions: match: "^(\/|[a-z0-9-.]+|{[a-zA-Z0-9_]+})+$" ``` The [Speakeasy CLI](https://www.speakeasy.com/docs/speakeasy-reference/cli/getting-started) tool also provides [linting capabilities](https://www.speakeasy.com/docs/prep-openapi/linting). With the Speakeasy CLI installed, run the following command to lint your OpenAPI document: ```bash speakeasy lint openapi -s openapi.yaml ``` This will output any issues found in your OpenAPI document: ![Screenshot of a terminal showing output from the Speakeasy CLI lint command](/assets/blog/enforcing-api-consistency/speakeasy-lint.png) If you're using Speakeasy to generate SDKs, you can configure it to lint your OpenAPI document as part of the generation process. This ensures that your OpenAPI document is always up-to-date and consistent with your generated code. Speakeasy supports custom rules in the Spectral format, so you can enforce your organization's standards in the same way as with Spectral. Individual team members can run these checks locally, before pushing their changes. This reduces the burden on reviewers and ensures that issues are caught early, when they're easiest to fix. In fact, [Spectral](https://marketplace.visualstudio.com/items?itemName=stoplight.spectral) provides a VS Code extension with linting features, so you can stay consistent without leaving your IDE. Shift left! Linters can also be run as part of your CI/CD pipeline so that no changes are merged without passing these checks and inconsistencies are caught before they reach staging or production. #### Contract testing Verify that your API implementations match their specifications. Tools like [Pact](/post/pact-vs-openapi) can help you write tests that verify that your API behaves as expected. OpenAPI itself is not a contract testing tool in itself, but it can be used as a source of truth for contract tests. Speakeasy supports [contract testing](/docs/sdk-testing) for OpenAPI documents. You can generate contract tests from your OpenAPI document and run them as part of your CI/CD pipeline. Speakeasy generates test workflows using [Arazzo](https://www.speakeasy.com/openapi/arazzo) (formerly known as OpenAPI Workflows), a simple, human-readable specification for API workflows. This allows you to extend the generated tests with custom logic, making it easy to test complex workflows across multiple services. Automated contract tests ensure that your API implementations match their specifications, reducing the risk of inconsistencies. #### Code generation Generate server stubs and SDKs from your OpenAPI definitions. This ensures that your implementation matches your specification and provides consistent interfaces across languages. [Speakeasy generates SDKs](https://www.speakeasy.com/docs/sdks/create-client-sdks) in multiple languages from your OpenAPI document. #### Integration tests Write automated tests that verify cross-service behavior, especially around authentication, error handling, and common workflows. Better yet, [generate these tests](/docs/sdk-testing) from your OpenAPI definitions. ### Human review where it matters Some aspects of API design can't be automated and need human judgment: 1. **Domain alignment:** Are your API abstractions aligned with your business domain? This requires deep understanding of both your technical architecture and business context. 2. **Developer experience:** Is your API intuitive and easy to use? This often requires user research and feedback from actual developers. 3. **Breaking changes:** Will a proposed change break existing clients? Humans need to evaluate the impact of each proposed change and plan appropriate migration paths. 4. **Cross-team impacts:** How will changes affect other teams and services? This requires a thorough understanding of system dependencies and team dynamics. ### Establish clear processes Create lightweight processes that combine automation with human judgment: 1. **API design reviews:** Start with automated checks, then focus human review on what matters. Run automated linting and validation first, then: - If the checks pass, reviewers focus on domain alignment and developer experience. - If the checks fail, fix the basic issues before involving more people. 2. **Regular API audits:** Periodically review your APIs as a whole: - Run consistency reports across all services. - Identify patterns of drift. - Update standards based on what's working. - Deprecate patterns that cause problems. 3. **Documentation and training:** Help teams understand and apply standards: - Maintain living documentation of your standards. - Provide clear examples of good and bad patterns. - Run workshops on API design. - Share case studies of successful and problematic APIs. ### When standards need to change Standards shouldn't be static. As your organization grows and your APIs evolve, your standards will need to change too. When updating standards: 1. **Start small:** Test changes with one team before rolling out widely. 2. **Provide migration paths:** Don't force immediate updates to existing APIs. 3. **Document clearly:** Explain what changed and why. 4. **Update tooling:** Ensure your automated checks align with new standards. Tests that consistently fail should be updated as soon as possible. If a test is failing because it's outdated, it's not serving its purpose, and will degrade trust in your automated checks over time. ### Document the why At Speakeasy, we're big fans of *starting with the why*. In internal discussions and pull requests, we often ask what problem we're trying to solve before presenting solutions. This helps us understand the context, make better decisions, and provide better feedback. When documenting standards, it's important to explain why they exist. This helps developers understand the reasoning behind the rules and makes it easier to follow them. It also makes it easier to update standards when they're no longer relevant. One way of keeping tabs on temporary inconsistencies is to document them as exceptions, each with a reference number to a ticket or a discussion. This way, you can track them and decide whether they should be resolved or documented as permanent exceptions. ### Reaching consensus across teams When teams disagree on standards, try to understand the degree to which the inconsistency matters. If it's a non-negotiable, like authentication or error handling, it's worth spending the time required to reach consensus. If it's a naming convention or a performance optimization, it may not be worth the effort. Use lightweight signals (like the [Internet Engineering Task Force (IETF) humming](https://en.wikipedia.org/wiki/Consensus_decision-making#IETF_rough_consensus_model)) to gauge general direction while concentrating on resolving voiced concerns. Rough consensus is often enough to move forward. If a team has a strong reason for doing something differently, it's worth considering whether the standard should be updated. Consensus is easier to reach if everyone understands the problem clearly. Once again, using a design-first approach with OpenAPI can help rule out any misunderstandings. If everyone is working from the same source of truth, it's easier to understand and compare one another's perspectives. ### Making it sustainable Perfect consistency isn't the goal. The goal is making your APIs predictable and easy to use, while allowing for necessary variation. Focus on the patterns that matter most to your developers and be pragmatic about enforcing them. ## Building a culture of consistency When done well, consistency reduces developers' cognitive load, speeds up integration, and makes your APIs more maintainable. When done poorly, it becomes a bureaucratic burden that slows teams down and encourages workarounds. Before reaching for the "API governance team" hammer, consider the following: 1. Automate everything that can be automated, but don't try to automate judgment calls. 2. Focus human review on what matters most: domain alignment, developer experience, and cross-team impacts. 3. Keep processes lightweight and focused on enabling teams rather than controlling them. 4. Allow standards to evolve based on real-world feedback and changing needs. 5. If you need to be flexible, document why. The goal is to create a culture where consistency is valued and maintained by everyone, rather than enforced by a select few, or worse, ignored by all. This article is part of our series on [API design](/post/api-design), where we get technical about building APIs that developers love. If you're interested in learning more about API design, check out our other articles in the series. # Update all connected repositories Source: https://speakeasy.com/blog/faster-better-speakeasy-run-commands We're excited to announce three updates to our `speakeasy run` command that make SDK generation faster, more flexible, and easier to manage at scale. ## What we made ### Concurrent execution support `speakeasy run` now processes multiple local repositories simultaneously, reducing total execution time when working across your local API ecosystem. In other words, instead of processing local repos one by one (Repo A → Repo B → Repo C), it now processes them all at the same time (Repo A + Repo B + Repo C in parallel). ### Multi-repository management with `--github-repos` Run SDK generation across all your connected GitHub repositories with a single command. Whether you want to update all repositories at once with `speakeasy run --github-repos all` or target specific ones with `speakeasy run --github-repos "org/repo1,org/repo2"`, you now have the flexibility to manage your entire SDK ecosystem from one place. ```bash speakeasy run --github-repos all # Target specific repositories speakeasy run --github-repos "org/repo1, org/repo2" ``` ### Flexible output formatting with `--output` Choose how you want to see `speakeasy run` output: - `console` - Traditional detailed output for debugging and development - `summary` - A more concise version — perfect for CI/CD pipelines - `mermaid` - Visual diagram output for documentation and reporting --- #### Console Output ```bash speakeasy run --output console ``` ![Console output showing detailed speakeasy run execution](/assets/blog/faster-better-speakeasy-run-commands/speakeasy-run-output-console.png) #### Summary Output ```bash speakeasy run --output summary ``` ![Summary output showing concise speakeasy run results](/assets/blog/faster-better-speakeasy-run-commands/speakeasy-run-output-summary.png) #### Mermaid Diagram Output ```bash speakeasy run --output mermaid ``` ![Mermaid diagram output showing visual representation](/assets/blog/faster-better-speakeasy-run-commands/speakeasy-run-output-mermaid.png) ## Why it matters **Concurrent execution:** means teams spend less time waiting and more time shipping. What used to require sequential runs across multiple repositories can now happen in parallel. **Scale without complexity**: As your API landscape grows, managing SDK generation across dozens of repositories can become unwieldy. The `--github-repos` flag enables users to make updates across multiple repos with a single command. **Context-appropriate information**: Different situations call for different levels of detail. Whether you're debugging locally (use `console`), running in CI (use `summary`), or creating documentation (use `mermaid`), you get exactly the information you need without the noise. These improvements are particularly valuable for platform teams managing multiple APIs, DevOps engineers integrating SDK generation into CI/CD workflows, and any organization looking to streamline their API-first development process. ## I'm interested — how do I get started? All these features are available from Speakeasy CLI version 1.548.0 onwards — just run `speakeasy update` if necessary. # fundraising-series-a Source: https://speakeasy.com/blog/fundraising-series-a import { Callout } from "@/mdx/components"; We've raised $15M to revolutionize REST API development. With this latest round of investment, we're expanding our product offerings, accelerating our roadmap, and growing our exceptional team 🚀 If software is eating the world, APIs are the knives and forks. They're not just connecting systems: they're fundamentally reshaping how we build, deploy, and scale software. Slack aside, service-based architectures are how engineering teams are communicating with one another internally. Meanwhile, every B2B SaaS is working to transform into an API-based company and offer programmatic access to their products. These trends have made APIs the #1 source of web traffic and they're growing faster than any other type. With the rise of AI, we're on the cusp of an even more dramatic surge in traffic as machine-to-machine communication explodes. But against this backdrop of rapid API growth, the tools and practices for building quality, reliable APIs haven't kept pace. This growing gap is creating a bottleneck that could choke the next wave of software innovation. Unless we invest in better API development solutions, we risk stifling the very progress that APIs have enabled. ## No more API embarrassment In our conversations with countless engineering teams, we've discovered a common but rarely discussed emotion surrounding their APIs: embarrassment. It's consistent across companies of all sizes and industries. Teams composed of brilliant engineers find themselves disappointed that their company's APIs don't represent the best of their team's abilities. They're embarrassed by: - Documentation that's perpetually out of date - Inadvertent breaking changes that slip into production - Inconsistencies in naming conventions and functionality - SDKs that lag behind API updates, if they exist at all There's a disconnect between the quality these teams aspire to and the reality of their APIs. Why? Are developers inherently bad at designing and building APIs? We don't think so. The root of the problem lies with the tools available. Even the most gifted craftsman needs the appropriate tools to build something they're proud of. And unfortunately the API tooling ecosystem is stuck in 2005. ## Why we're building Speakeasy We want to make it trivially easy for every developer to build great APIs. Said another way, no more API embarrassment. To make that dream a reality, we're building the API tools we've always wanted: a platform to handle the heavy lifting of API development to unburden developers to focus on refining the business logic. Here's the future we want to create: 1. Build: You use your favorite API framework to build your API. Speakeasy helps you make sure that your API matches industry & internal best practices. 2. Test: You develop new API features. Speakeasy automates API testing to make sure no unintentional breaking changes get shipped. 3. Distribute: You release your API. Speakeasy automatically generates the SDKs that make your API a joy to integrate.
## Why we raised money We partnered with FPV Ventures to lead our $15M Series A funding round, with continued support from GV (formerly Google Ventures) and Quiet Capital. Our investors, alongside angels like Søren Bramer Schmidt (CEO at Prisma), Clint Sharp (Co-founder at Cribl), and Arpit Patel (CCO at Traceable), have provided us with the fuel we need to revolutionize API development for engineers everywhere. We asked Wesley Chan, Managing Partner at FPV Ventures, why he invested: > "The Speakeasy team's past experience building enterprise APIs has given them profound insight into, and empathy for, the struggles engineering teams are facing. They are building a platform that will not only address existing inefficiencies in API development but anticipates future challenges in the ecosystem. But what ultimately convinced us to lead their round was the overwhelming feedback from their user base. Teams aren't just using the platform; they're enthusiastically championing it, which speaks volumes about Speakeasy's ability to deliver real value and an exceptional experience." Of course, what ultimately matters isn't what investors think, it's what developers choose. This year, we've seen nearly 3,000 users generate 7,250 SDKs. That's a 575% increase in companies investing in their API's tooling. From fast-growing innovators like Vercel and Mistral AI to established giants like Verizon, to hobby projects by solo devs, engineers are using Speakeasy to accelerate their API development and adoption. import Image from "next/image";
We're proud of the progress we've made so far, but we can also say with certainty that we are only getting started. With this new funding, we're expanding our product offerings, accelerating our roadmap, and growing our exceptional team across San Francisco and London. We have ambitious plans to revolutionize contract testing, API creation, and webhook management. Ready to supercharge your API lifecycle? Let's talk. **Sagar & Simon, Co-founders, Speakeasy** # Read-only mode Source: https://speakeasy.com/blog/generate-mcp-from-openapi AI agents are becoming a standard part of software interaction. From Claude Desktop assisting with research to custom agents automating workflows, these tools need a reliable way to interact with APIs. This is where the [Model Context Protocol (MCP)](https://modelcontextprotocol.io/) comes in. Introduced by Anthropic in November 2024, MCP provides a universal standard for connecting AI agents to APIs and data sources. Think of it as the bridge between natural language and your API endpoints. If you already have an API documented with OpenAPI, you're in luck. You can automatically generate a fully functional MCP server from your existing OpenAPI document. In this guide, we'll look at four tools that make this possible: Speakeasy's SDK generation, the Gram platform, FastMCP for Python developers, and the open-source openapi-mcp-generator. ## What is MCP and why does it matter? The Model Context Protocol is an open standard that enables AI agents to interact with APIs in a consistent way. Instead of building custom integrations for each AI platform, you create one MCP server that works across compatible AI tools like Claude Desktop, Cursor, and others. MCP follows a client-server architecture where AI applications act as clients, and your MCP server exposes API operations as "tools" that agents can discover and use. These tools are structured descriptions of what your API can do - complete with parameters, expected inputs, and return types. When an AI agent needs to accomplish a task that requires your API, it queries the MCP server for available tools, selects the appropriate one, and executes it with the necessary parameters. ### From OpenAPI to MCP: Where generators fit in Now, why does this matter if you already have an OpenAPI document? Because OpenAPI specs already contain everything needed to create MCP tools: - **Endpoint paths and methods** define what operations are available - **Parameters and request schemas** specify what inputs each operation needs - **Response schemas** describe what data comes back - **Operation descriptions** explain the purpose of each endpoint An MCP generator transforms your OpenAPI specification into a functioning MCP server that exposes your API endpoints as tools AI agents can use: ![Generation explained](/assets/blog/generate-mcp-from-openapi/generation-explained.png) Your OpenAPI document serves as the source of truth, with its operations documented in OpenAPI format. An MCP generator reads this document and automatically creates tool definitions for each endpoint. The generated MCP server then acts as a bridge, translating AI agent requests into proper API calls. AI agents can discover and use your API's capabilities through the MCP server, without needing to understand your API's specific implementation details. Your OpenAPI document becomes the single source of truth, eliminating the need to manually maintain two separate specifications. Your MCP server stays in sync with your API automatically. ## The challenge: Building MCP servers manually While the MCP specification is well-documented, building a server from scratch involves significant work: - **Understanding the protocol**: MCP uses JSON-RPC 2.0 for transport and has specific conventions for tool definitions, resource handling, and error responses. - **Keeping tools in sync**: Every time your API changes, you need to manually update tool definitions to match. - **Type safety and validation**: You'll need to implement request validation and ensure type safety across the entire chain. - **Hosting and deployment**: MCP servers need infrastructure for hosting, whether locally for development or remotely for team-wide access. For teams that already maintain OpenAPI documents, duplicating this effort in MCP format creates unnecessary maintenance burden. This is where automation helps. ## Four tools for generating MCP servers from OpenAPI documents In this post we'll look at four platforms and tools that automatically generate MCP servers from OpenAPI documents: - **[Speakeasy](https://www.speakeasy.com/docs/standalone-mcp/build-server)** generates TypeScript SDKs and MCP servers together, giving you full control over the generated code and deployment. - **[Gram](https://getgram.ai)** is a managed platform made by Speakeasy that provides instant hosting and built-in toolset curation. - **[FastMCP](https://github.com/jlowin/fastmcp)** is a Pythonic framework that converts OpenAPI specs and FastAPI applications into MCP servers with minimal code. - **[openapi-mcp-generator](https://github.com/harsha-iiiv/openapi-mcp-generator)** is an open-source CLI tool that generates standalone TypeScript MCP servers. These tools differ primarily in their hosting models. **Managed platforms** like Gram and FastMCP Cloud handle hosting and infrastructure for you - on Gram you upload your OpenAPI document and you get an instantly accessible MCP server. **Self-hosted** tools like Speakeasy and openapi-mcp-generator generate code that you deploy and run yourself, giving you full control over infrastructure, customization, and deployment. Here's how they compare across hosting model and automation level: ![Comparing MCP server generators](/assets/blog/generate-mcp-from-openapi/comparing-mcp-server-generators.png) **[Gram](https://getgram.ai)** offers the fastest path to production with a fully managed platform - no infrastructure to maintain. **[Speakeasy](https://www.speakeasy.com/docs/standalone-mcp/build-server)** provides comprehensive code generation for self-hosted deployment. **[FastMCP](https://github.com/jlowin/fastmcp)** gives you both options: use the Python framework for self-hosted servers or FastMCP Cloud for managed hosting. **[openapi-mcp-generator](https://github.com/harsha-iiiv/openapi-mcp-generator)** generates standalone TypeScript servers for complete self-hosted control. ## MCP server generators comparison Here's a more detailed comparison of the features of each: | Feature | Speakeasy | Gram | FastMCP | openapi-mcp-generator | |----------------------|-----------------------|---------------|---------------------|-----------------------| | **Language** | TypeScript | N/A (hosted) | Python | TypeScript | | **Setup complexity** | Low | Low | Low | Low | | **Customization** | Full code access | Config-based | Programmatic | None | | **Tool curation** | N/A | Yes, built-in | Programmatic | None | | **Hosting** | Self-hosted | Managed | Self-hosted/Managed | Self-hosted | | **Type safety** | Full (Zod) | N/A | Partial | Full (Zod) | | **SDK generation** | Yes (7+ langs) | No | No | No | | **Auth handling** | OAuth 2.0 | OAuth 2.0 | Manual config | Env variables | | **Test clients** | Generated Test Client | Playground | No | HTML clients | Next, we'll explore each tool in detail. ## Speakeasy: SDK + MCP server generation [Speakeasy](https://www.speakeasy.com/docs/standalone-mcp/build-server) is an SDK generation platform that creates production-ready SDKs from OpenAPI documents. When you generate a TypeScript SDK with Speakeasy, you also get a complete MCP server implementation. ### How it works When Speakeasy processes your OpenAPI document, it generates a type-safe TypeScript SDK using Zod schemas for validation, creates an MCP server that wraps the SDK methods, transforms each SDK operation into an MCP tool with proper type definitions, and provides a CLI for starting and configuring the server. The generated MCP server includes: ```bash mcp-server/ ├── tools/ # Each API operation as an MCP tool │ ├── listTasks.ts │ ├── createTask.ts │ ├── getTask.ts │ └── updateTask.ts ├── server.ts # Main MCP server implementation ├── scopes.ts # Scope-based access control └── cli.ts # Command-line interface ``` ### Example: Task management API Let's say you have a simple task management API with this OpenAPI operation: ```yaml openapi.yaml paths: /tasks: post: operationId: createTask summary: Create a new task description: Creates a new task with the provided details requestBody: required: true content: application/json: schema: type: object required: - title properties: title: type: string description: Task title description: type: string description: Detailed task description priority: type: string enum: [low, medium, high] responses: '201': description: Task created successfully content: application/json: schema: $ref: '#/components/schemas/Task' ``` Speakeasy generates an MCP tool that looks like this: ```ts tools/createTask.ts import { createTask } from "../../funcs/createTask.js"; import * as operations from "../../models/operations/index.js"; import { formatResult, ToolDefinition } from "../tools.js"; const args = { request: operations.CreateTaskRequest$inboundSchema, }; export const tool$createTask: ToolDefinition = { name: "create-task", description: "Create a new task\n\nCreates a new task with the provided details", args, tool: async (client, args, ctx) => { const [result, apiCall] = await createTask(client, args.request, { fetchOptions: { signal: ctx.signal }, }).$inspect(); if (!result.ok) { return { content: [{ type: "text", text: result.error.message }], isError: true, }; } return formatResult(result.value, apiCall); }, }; ``` The tool is type-safe, handles errors gracefully, and includes full request validation using Zod schemas. ### Customizing with x-speakeasy-mcp Speakeasy supports the `x-speakeasy-mcp` OpenAPI extension for fine-tuning your MCP tools: ```yaml paths: /tasks: post: operationId: createTask summary: Create a new task x-speakeasy-mcp: name: "create_task_tool" description: | Creates a new task in the task management system. Use this when the user wants to add a new task or todo item. Requires a title and optionally accepts a description and priority level. scopes: [write, tasks] ``` This allows you to provide AI-specific descriptions and organize tools using scopes. ### Using scopes for access control Scopes let you control which tools are available in different contexts: ```yaml paths: /tasks: get: x-speakeasy-mcp: scopes: [read] post: x-speakeasy-mcp: scopes: [write] /tasks/{id}: delete: x-speakeasy-mcp: scopes: [write, destructive] ``` Start the server with specific scopes: ```bash npx mcp start --scope read # Read and write, but not destructive operations npx mcp start --scope read --scope write ``` ### When to use Speakeasy Choose Speakeasy if you need full control over generated code and deployment, want type-safe SDKs alongside your MCP server, require extensive customization of the MCP implementation, or prefer self-hosting with complete infrastructure control. ## Gram: Managed MCP platform [Gram](https://getgram.ai) is a managed platform made by Speakeasy that takes a different approach to MCP server generation. Instead of generating code for you to deploy, Gram builds on Speakeasy's excellent MCP generation to provide a fully hosted platform where you upload your OpenAPI document and get an instantly accessible MCP server. ### How Gram works Upload your OpenAPI document and the platform parses your API specification. Create toolsets by selecting and organizing relevant operations into use-case-specific groups. Configure environments with API keys and environment variables. Deploy and your MCP server is immediately available at a hosted URL. ### Enterprise-ready features Gram provides a comprehensive platform built for teams and production deployments: **Toolset curation**: Not every API operation makes sense as an MCP tool. Gram lets you select specific operations to include or exclude, create multiple toolsets for different use cases (like "read-only" vs "full-access"), add custom prompts and context to individual tools, and combine operations into workflow-based custom tools. ![Gram tools curate](/assets/blog/generate-mcp-from-openapi/gram-tools-curate.png) **Environment management**: Configure multiple environments (development, staging, production) with different API keys, base URLs, and credentials. Switch between environments without changing your MCP server configuration. **Built-in authentication**: Gram handles OAuth flows, API key management, and token refresh automatically. Your MCP server can authenticate with APIs that require OAuth without you needing to implement the flow yourself. ![Gram authentication](/assets/blog/generate-mcp-from-openapi/gram-auth.png) **Team collaboration**: Share toolsets across your organization, manage access controls, and collaborate on tool definitions with your team. **Managed hosting**: Instant deployment at `app.getgram.ai/mcp/your-server` or use custom domains like `mcp.yourdomain.com`. No infrastructure to manage, no servers to maintain. **Interactive playground**: Test your tools directly in Gram's playground before deploying to production. Try natural language queries and see exactly how AI agents interact with your API. ### The x-gram extension Similar to Speakeasy, Gram supports OpenAPI extensions for customization: ```yaml paths: /tasks/{id}: get: operationId: getTask summary: Get task details x-gram: name: get_task_details description: | Retrieves complete details for a specific task including title, description, priority, status, and timestamps. - You must have a valid task ID. Use list_tasks first if needed. responseFilterType: jq ``` The `x-gram` extension lets you provide LLM-optimized descriptions with context, specify prerequisites for using a tool, and configure response filtering to reduce token usage. ### When to use Gram Choose Gram if you want the fastest path from an OpenAPI document to a hosted MCP server, prefer managed infrastructure over self-hosting, need multiple toolsets for different use cases, require built-in OAuth and API key management, or value ease of use and team collaboration over infrastructure control. Sign up today at [getgram.ai](https://getgram.ai) to get started. ## FastMCP: Python framework for MCP servers [FastMCP](https://github.com/jlowin/fastmcp) is a Python framework for building MCP servers. As of [FastMCP version `2.0.0`](https://github.com/jlowin/fastmcp/releases/tag/v2.0.0) it can automatically convert OpenAPI specifications into MCP servers with just a few lines of code. FastMCP also supports converting FastAPI applications directly, making it ideal for Python developers who want to expose existing FastAPI endpoints as MCP tools. ### How FastMCP works By default, every endpoint in your OpenAPI specification becomes a standard MCP Tool, making all your API's functionality immediately available to LLM clients. You can customize which endpoints to include and how they're exposed using route mapping. ### Getting started with FastMCP Install FastMCP: ```bash pip install fastmcp ``` Create an MCP server from OpenAPI: ```python import httpx from fastmcp import FastMCP # Load your OpenAPI spec client = httpx.AsyncClient(base_url="https://api.example.com") openapi_spec = httpx.get("https://api.example.com/openapi.json").json() # Create MCP server mcp = FastMCP.from_openapi( openapi_spec=openapi_spec, client=client, name="Task Management API" ) # Run the server mcp.run() ``` Configure Claude Desktop: ```json { "mcpServers": { "tasks-api": { "command": "python", "args": ["path/to/your/mcp_server.py"] } } } ``` ### Customizing route mapping FastMCP allows you to customize how OpenAPI endpoints are converted: ```python from fastmcp import FastMCP, RouteMap # Create custom route mapping route_map = RouteMap() # Exclude specific endpoints route_map.exclude("/internal/*") # Convert specific routes to Resources instead of Tools route_map.map("/tasks", component_type="resource") mcp = FastMCP.from_openapi( openapi_spec=openapi_spec, client=client, name="Task Management API", route_map=route_map ) ``` ### FastMCP Cloud: Managed hosting FastMCP also offers [FastMCP Cloud](https://fastmcp.cloud), a managed hosting platform similar to Gram. With FastMCP Cloud, you can upload your OpenAPI document or deploy your FastMCP server and get instant hosted access without managing infrastructure. ![FastMCP Cloud](/assets/blog/generate-mcp-from-openapi/fastmcp-cloud.png) ### When to use FastMCP Choose FastMCP if you work primarily in Python, want minimal boilerplate code, need programmatic control over server configuration, or prefer a lightweight, code-first approach. Use FastMCP Cloud for managed hosting while staying in the Python ecosystem. FastMCP recommends manually designed MCP servers for complex APIs to achieve better performance. The auto-conversion is best for getting started quickly or for simpler APIs. ## openapi-mcp-generator: Open-source TypeScript tool [openapi-mcp-generator](https://github.com/harsha-iiiv/openapi-mcp-generator) is an open-source CLI tool that generates standalone TypeScript MCP servers from OpenAPI specifications. ![openapi-mcp-generator](/assets/blog/generate-mcp-from-openapi/mcp-generator.png) ### How it works The generator creates a complete TypeScript project with automatic Zod validation schemas, multiple transport mode support (stdio, web server with SSE, StreamableHTTP), built-in HTML test clients, environment-based authentication, and fully typed server code. ### Getting started Install the generator: ```bash npm install -g openapi-mcp-generator ``` Generate a stdio MCP server (for Claude Desktop): ```bash openapi-mcp-generator \ --input path/to/openapi.json \ --output path/to/output/dir ``` Or generate a web server with Server-Sent Events: ```bash openapi-mcp-generator \ --input path/to/openapi.json \ --output path/to/output/dir \ --transport=web \ --port=3000 ``` Run the generated server: ```bash cd path/to/output/dir npm install npm start ``` ### Transport modes The generator supports three transport modes: **stdio**: Standard input/output for desktop applications like Claude Desktop ```json { "mcpServers": { "tasks-api": { "command": "node", "args": ["path/to/generated/server/index.js"], "env": { "API_KEY": "your-api-key" } } } } ``` **web**: HTTP server with Server-Sent Events for browser-based clients ```bash openapi-mcp-generator --input openapi.json --output ./server --transport=web --port=3000 ``` **StreamableHTTP**: Streaming HTTP for high-performance scenarios ```bash openapi-mcp-generator --input openapi.json --output ./server --transport=streamable-http ``` ### Authentication The generator handles authentication via environment variables: ```bash # In your .env file or environment API_KEY=your-api-key-here API_BASE_URL=https://api.example.com ``` The generated server automatically includes these in requests to your API. ### When to use openapi-mcp-generator Choose this tool if you want a basic, standalone MCP server without additional complexity. It's a straightforward CLI that generates TypeScript code from your OpenAPI spec - what you see is what you get. You can edit the generated server code afterward if needed, but there's zero customization during generation beyond choosing transport modes. Best suited for simple use cases where you need a functional MCP server quickly without enterprise features, managed hosting, or advanced tooling. ## Optimizing your OpenAPI document for MCP Regardless of which tool you choose, the quality of your resulting MCP tools depends on the quality of your OpenAPI document. AI agents need more context than human developers to use APIs effectively. ### Write for AI agents, not just humans Humans can infer context from brief descriptions. AI agents cannot. Compare these descriptions: **Basic description** (for humans): ```yaml get: summary: Get task description: Retrieve a task by ID ``` **Optimized description** (for AI agents): ```yaml get: summary: Get complete task details description: | Retrieves full details for a specific task including title, description, priority level (low/medium/high), current status, assignee, due date, and creation/update timestamps. Use this endpoint when you need complete information about a task. If you only need a list of task IDs and titles, use listTasks instead. ``` The optimized version tells the AI agent what data to expect in the response, when to use this endpoint vs alternatives, and what each field means (priority values, status types). ### Provide clear parameter guidance Include examples and explain constraints: ```yaml parameters: - name: task_id in: path required: true schema: type: string format: uuid description: | The unique identifier for the task. Must be a valid UUID v4. You can get task IDs by calling listTasks first. examples: example1: summary: Valid task ID value: "550e8400-e29b-41d4-a716-446655440000" ``` ### Add response examples Example responses help AI agents understand what successful responses look like: ```yaml responses: '200': description: Task retrieved successfully content: application/json: schema: $ref: '#/components/schemas/Task' examples: complete_task: summary: Task with all fields populated value: id: "550e8400-e29b-41d4-a716-446655440000" title: "Review Q4 goals" description: "Review and update quarterly objectives" priority: "high" status: "in_progress" assignee: "alice@example.com" due_date: "2025-10-15" created_at: "2025-10-01T09:00:00Z" updated_at: "2025-10-07T14:30:00Z" ``` ### Use descriptive operation IDs Operation IDs become tool names. Make them clear and action-oriented: ```yaml # Good operationId: createTask operationId: listActiveTasks operationId: archiveCompletedTasks # Less clear operationId: post_tasks operationId: get_tasks_list operationId: update_task_status ``` ### Document error responses Help AI agents understand what went wrong: ```yaml responses: '404': description: | Task not found. This usually means: - The task ID is incorrect - The task was deleted - You don't have permission to view this task content: application/json: schema: $ref: '#/components/schemas/Error' ``` ## Conclusion Generating MCP servers from OpenAPI documents bridges the gap between your existing APIs and AI agents. With an OpenAPI document you can have a working MCP server in minutes instead of days. Ready to get started? Try [Gram](https://getgram.ai) today to get your MCP server hosted instantly. The era of AI agents is here. Make sure your API is ready for it. # generating-mcp-from-openapi-lessons-from-50-production-servers Source: https://speakeasy.com/blog/generating-mcp-from-openapi-lessons-from-50-production-servers Suddenly, everyone wants an MCP server. You built an API for humans – now AI agents need to use it. That means you need an MCP server. What if you could just point to your OpenAPI document and generate one? Actually, you can. We've [built a tool](/docs/standalone-mcp/build-server) that automatically generates an MCP server from your OpenAPI document. So far, we've used it to [generate over 50 MCP servers](https://github.com/search?q="speakeasy"+path%3A**%2Fsrc%2Fmcp-server%2Fmcp-server.ts&type=code) for customers, with many already running in production. But generating MCP servers from OpenAPI isn't trivial. In the post [Auto-generating MCP Servers from OpenAPI Schemas: Yay or Nay?](https://neon.tech/blog/autogenerating-mcp-servers-openai-schemas), Neon captures many of the challenges, such as: - An overwhelming decision space: MCP servers tend to have as many tools as there are operations, which is problematic for APIs with more than 500 operations, like GitHub's. - Fragile interactions with complex JSON inputs. - Difficulties with LLMs understanding workflows from the OpenAPI document, like recognizing that users add items to a cart before validating and paying. To address those challenges, Neon recommends a hybrid approach: > [Look] at the tools for generating an MCP server from OpenAPI specs, then begin aggressively pruning and removing the vast majority of the generated tools, keeping only low-level operations that represent genuinely useful, distinct capabilities that an LLM might perform. This is the approach Speakeasy takes, but our generator automates pruning and supports customization for greater specificity. After generating more than 50 production MCP servers, we've seen what breaks, what matters, and what to avoid. The lessons that follow will help you build and optimize your own MCP server, whether you use our tool or not. ## A short terminology refresher MCP is a fast-evolving space and can get complex, but for this guide, you only need to understand a few key concepts: - An **OpenAPI document** is a YAML or JSON file that describes your API, from endpoints to fields to schemas for request payloads, successful responses, and errors. See more on the [Speakeasy OpenAPI hub](/openapi). - A **generator** is a tool that takes an OpenAPI document as input and produces an artifact. Previously, the Speakeasy generator focused on creating SDKs and documentation to help users interact with our clients' APIs. Now it generates MCP servers, too. - **[MCP](https://modelcontextprotocol.io/introduction)** is a protocol for AI agents to interact with your API. - A **[tool](/mcp/tools)** is a function that an agent can call. An MCP Tool consists of the following components: - Name - Schema - Description The description can be seen as the "prompt". You need a high-quality description to ensure agents accurately and correctly identify the tool they need to call for a specific action. ![MCP servers generated from OpenAPI](/assets/blog/generating-mcp-from-openapi-lessons-from-50-production-servers/openapi-generator-mcp-tools.png) ## Optimizing OpenAPI documents for MCP servers Being verbose in OpenAPI documents is normal when they're used to generate SDKs and API documentation. Since we, humans, will read and interact with these artifacts, it's intuitive to repeat key information across endpoint, schema, or field descriptions. This helps readers understand things in context without having to jump around the entire document. However, more words mean more characters and therefore more tokens consumed in the LLM's context window. LLMs prefer concise and direct descriptions. Since LLMs process all tool and field-level descriptions at once, unlike humans, this creates a strong reason to modify the OpenAPI document for improved token usage. So, how do you balance being both concise and clear while avoiding repetition? The truth is, it's impossible without drastically affecting your OpenAPI document, which must serve both the MCP server's needs and API documentation. That's why at Speakeasy, we shifted the optimization layer across three components: - **The OpenAPI document:** This serves as your single source of truth, so you want to make as many changes directly here as possible. However, how much you can modify the document depends on balancing your MCP server's needs without compromising the clarity or usability of your API documentation and SDKs. - **The generator itself:** It handles aspects like data formats, streaming, and other common API behaviors that don't work well with agents. - **A custom function file:** Located alongside your generated MCP server, this lets you precisely control how specific tools behave. Shifting the optimization layer helps us avoid manual changes directly on the generated MCP server that would need to be repeated after every regeneration. ![Manual customization on the MCP server](/assets/blog/generating-mcp-from-openapi-lessons-from-50-production-servers/mcp-server-generation-manual-flow.png) Instead, our approach creates a workflow that allows us to elegantly control the MCP server while enabling regeneration at any time without losing customizations. ![Manual customization on the OpenAPI document and MCP server generator](/assets/blog/generating-mcp-from-openapi-lessons-from-50-production-servers/mcp-server-automated-work.png) This change in workflow taught us how to tackle common problems when generating production-ready MCP servers. We addressed these challenges by adding customization options both in the OpenAPI document and within the MCP server generator. Let's take a closer look at the issues and our solutions. ## Too many endpoints = too many tools Let's say you have 200 endpoints in your OpenAPI document. Generating an MCP server from this document will easily create around 200 tools. Now, assume you have 200 buttons in front of you, with a vague initial premise. You'd struggle to find the right button to press, whether it's taking time to analyze or taking a risk and pressing the wrong button. It's no different for LLMs. When faced with 200 tools, the model becomes confused as its context window is overwhelmed. Since many users rely on smaller models with even shorter context windows, the tool overload problem is more severe than it first appears. ### Our solution to tool explosion To resolve the tool explosion issue, start by pruning your OpenAPI document before generating MCP servers from it. Exclude non-useful endpoints (like `health/` and `inspect/`) and any that don't address the problem you're solving by using an MCP server. For example, say you're building an MCP server to help users interact with your e-commerce API by ordering items through an AI agent. Remove endpoints for user authentication, user management, and payments, and keep only those for browsing products, creating carts, and setting addresses. Another tactic for managing tool explosion is to disable the tools on the client side instead. Claude Desktop allows this, but if the server exposes over 200 tools, manually toggling them off one by one isn't much fun. ![Disabling tools in Claude Desktop](/assets/blog/generating-mcp-from-openapi-lessons-from-50-production-servers/disabling-mcp-tools.png) At Speakeasy, tool explosion was the first problem we needed to tackle, and fortunately, the easiest. Our generator looks for a custom `disabled` key in the OpenAPI document (defaulting to `false`). When set to `true`, a tool is not generated for this operation. ```yaml x-speakeasy-mcp: disabled: true ``` ### OpenAPI descriptions are not designed for LLMs Some OpenAPI documents include lengthy descriptions written for humans, not large language models (LLMs). These multi-paragraph descriptions often repeat the same details and add noise. The extra text increases token usage, and with many users relying on multiple MCP servers and smaller LLMs, it can fill the context window before the prompt is processed. As a result, the LLM chooses the wrong tool or hallucinates a response. However, short and vague descriptions also create issues. If several endpoints have similar names but do different things, the LLM won't know which one to use. Consider the following OpenAPI document snippet: ```yaml paths: /user/profile: get: summary: Get user profile description: Returns the user profile. responses: "200": description: OK content: application/json: schema: $ref: "#/components/schemas/UserProfile" /user/details: get: summary: Get user details description: Fetches user details. parameters: - name: userId in: query required: true schema: type: string responses: "200": description: OK content: application/json: schema: $ref: "#/components/schemas/UserDetails" /user/info: get: summary: Get user info description: Get user info. parameters: - name: userId in: query required: true schema: type: string responses: "200": description: OK content: application/json: schema: $ref: "#/components/schemas/UserPublicInfo" ``` Here, three endpoints return information about users. Because each operation has a vague description, the LLM may choose the wrong endpoint when making a call. ### Our solution to long or vague OpenAPI descriptions for MCP servers To avoid confusion, each operation should have a clear, precise description that explains exactly what it does and when to use it. ```yaml paths: /user/profile: get: summary: Get current user profile description: Retrieves the profile of the authenticated user, including display name, bio, and profile picture. responses: "200": description: OK content: application/json: schema: $ref: "#/components/schemas/UserProfile" /user/details: get: summary: Get internal user details by ID description: Retrieves detailed internal data for a specific user by ID, including email, role assignments, and account status. Requires admin access. parameters: - name: userId in: query required: true schema: type: string responses: "200": description: OK content: application/json: schema: $ref: "#/components/schemas/UserDetails" /user/info: get: summary: Get public user info description: Returns limited public-facing information for a specific user by ID, such as username and signup date. Useful for displaying user data in public or shared contexts. parameters: - name: userId in: query required: true schema: type: string responses: "200": description: OK content: application/json: schema: $ref: "#/components/schemas/UserPublicInfo" ``` However, you may still need a long description for your endpoint, especially if you are using your OpenAPI document for API references or developer documentation. To address this, Speakeasy supports the custom `x-speakeasy-mcp` extension for describing endpoints to LLMs. ```yaml paths: /products: post: operationId: createProduct tags: [products] summary: Create product description: API endpoint for creating a product in the CMS x-speakeasy-mcp: disabled: false name: create-product scopes: [write, ecommerce] description: | Creates a new product using the provided form. The product name should not contain any special characters or harmful words. # ... ``` And if you don't want to pollute your OpenAPI document with extensions or non-native terminologies, you can use an [overlay](/openapi/overlays), which is a separate document that modifies the OpenAPI document without directly editing the original. ## MCP servers struggle with complex formats Agents generally expect simple JSON responses, but APIs often return complex and varied payloads. For example, if you build an MCP server for an API based on the TM Forum OpenAPI specification, the payloads can be quite large and complicated. Since LLMs struggle with complex JSON formats, it's common for them to have difficulty processing such responses. For instance, an agent might see: - A streaming response, where the consumer is expected to keep a connection open until a stream of information has been completed. - A binary response, such as an image or audio file. - Unnecessary information included in responses, such as metadata. - Complex structures, such as content returned within a nested `{result}` field wrapped in an envelope, like the [`ProductOffering`](https://github.com/tmforum-apis/TMF620_ProductCatalog/blob/4a76d6bcef7ce63783ef2ffb93944cc9a9bbb075/TMF620-ProductCatalog-v4.1.0.swagger.json#L5780) object from the TM Forum [Product Catalog OpenAPI document](https://github.com/tmforum-apis/TMF620_ProductCatalog/blob/master/TMF620-ProductCatalog-v4.1.0.swagger.json). ### Our solution to MCP and complex formats Speakeasy handles complex formats by automatically transforming data before sending it to the MCP server. For example, if Speakeasy detects an image or audio file, it encodes it in Base64 before passing it to the LLM. This step is crucial because generating reliable MCP server code depends on accurately detecting data types and formatting them correctly for the MCP client. For streaming data, Speakeasy generates code that first streams the entire response and only passes it to the client once the stream completes. Speakeasy also allows you to customize how data is transformed. For instance, say you need to extract information from a CSV file returned in a response and convert it to JSON. You can write an [SDK hook](/docs/customize/code/sdk-hooks) to run after a successful request and before the response moves on to the next step in the SDK lifecycle. ## MCP servers expose everything Suppose you have a Salesforce MCP server, locally connected to Claude desktop. Even with restricted controls, you're one tool call away from leaking sensitive identity information or modifying accounts in unintended ways – whether due to hallucinations, missing context, or any of the issues we've already covered. This risk exists because MCP servers expose capabilities directly to the client. If you're using a custom MCP client, you can choose not to expose certain tools. With Claude desktop, you can toggle off specific tools to prevent them from being called. ![Disabling destructive tools in Claude desktop](/assets/blog/generating-mcp-from-openapi-lessons-from-50-production-servers/disabling-destructive-operation.png) However, things become complicated when you have multiple tools or descriptive actions. Managing this complexity across multiple clients or environments quickly becomes unscalable. So what if you could define these rules before the MCP server and clients are even generated? ### Our solution to MCP server access control We have a complementary approach to resolving access control issues. By using scopes, you can restrict tool use on the server rather than the client, and configure that behavior directly instead of relying on a UI like Claude desktop. This way, the server configuration provides built-in protection, regardless of which client the user is on. A scope is another kind of annotation you can apply to specific endpoints. For example, you can associate all `GET` requests with a `"read"` scope, and `POST`, `PUT`, `DELETE`, and `PATCH` methods with a `"write"` scope. ```yaml overlay: 1.0.0 info: title: Add MCP scopes version: 0.0.0 actions: - target: $.paths.*["get","head","query"] update: { "x-speakeasy-mcp": { "scopes": ["read"] } } - target: $.paths.*["post","put","delete","patch"] update: { "x-speakeasy-mcp": { "scopes": ["write"] } } ``` With scopes in place, you can start the server with a `read` scope and only expose the corresponding operations. Scopes are not limited to `read` and `write`. You can define custom scopes to control access to tools based on domain or functionality. For example, to limit the MCP server to exposing only operations related to products, you can add the scope `product` to the relevant endpoints. ```json { "mcpServers": { "MyAPI": { "command": "npx", "args": [ "your-npm-package@latest", "start", "--scope", "product" ], "env": { "API_TOKEN": "your-api-token-here" } } } } ``` ## Digression: A brief defense of OpenAPI In general discourse about APIs-to-AI-tools, OpenAPI sometimes gets an unfair bad reputation. The broad argument is that OpenAPI to MCP is simply ineffective. The thing to be clear about is that OpenAPI is a specification for describing APIs. It has no bearing on the quality or expansiveness of the API it describes. The real leap in maturity here is that increasingly, we want developers to build APIs suited for AI tools. ...and still you would want to describe these evolved APIs with OpenAPI. It's a great format and very compatible with MCP: - API endpoints can still map to tools, only now you have a more focused and well-documented set of them. - It uses JSON Schema same as MCP and beyond MCP can help power documentation sites, SDKs and other developer tools. This is not a new concept. In the world of frontend development, many teams were creating so-called [Backends For Front-ends][bff], or BFFs, that were composing multiple backend API services together to create more distilled APIs that were more immediately useful when building front-end apps. The responses from BFF API endpoints contained all the information needed to serve an app and avoided costly waterfalls of backend API calls that an otherwise heavily normalized REST API or many disjoint microservices would lead to. [bff]: https://samnewman.io/patterns/architectural/bff/ ## Final thoughts MCP servers are powerful tools that shape how users interact with AI agents. But given limitations like static context windows, insufficient descriptions, and the difficulty of handling complex data structures, they can quickly become sources of hallucinations and errors instead of enablers of great experiences for your users. At Speakeasy, we believe these issues can be mitigated by following a few best practices: - **Avoid tool explosion** by limiting the number of generated tools and focusing only on what's useful. - **Write clear, concise descriptions** for fields, schemas, and endpoints to help LLMs reason accurately. - **Transform complex data**, like binary files or deeply nested JSON, into simpler formats before sending the data to the client. - **Use scopes and Azzario documents** to restrict tool exposure and control tool generation by domain. # Manually configure what gets exposed Source: https://speakeasy.com/blog/gram-vs-fastmcp-cloud import { Callout, Table } from "@/lib/mdx/components"; import { CalloutCta } from "@/components/callout-cta"; import { GithubIcon } from "@/assets/svg/social/github"; This comparison of Gram & FastMCP is based on a snapshot of two developing products as of September 2025. If you think we need to update this post, please let us know! } title="Gram OSS Repository" description="Check out Github to see how it works under the hood, contribute improvements, or adapt it for your own use cases. Give us a star!" buttonText="View on GitHub" buttonHref="https://github.com/speakeasy-api/gram" /> Since Anthropic launched the Model Context Protocol in November 2024, the MCP ecosystem has grown quickly. But there's still a gap: building production-grade MCP servers takes too long, and maintaining them as your APIs evolve is tedious manual work. This post breaks down how [Gram](https://getgram.ai) and [FastMCP Cloud](https://fastmcp.cloud) each handle building and deploying MCP servers, and where they fit in your stack. This post compares Gram with FastMCP across two dimensions: building agent tools and deploying MCP servers. [FastMCP](https://gofastmcp.com/) is an open-source Python framework for building MCP servers, and [FastMCP Cloud](https://fastmcp.cloud/) is the managed hosting platform for deploying FastMCP-based applications. Gram is the MCP cloud, an open-source platform for building and deploying MCP servers. The platform includes Gram Functions, a TypeScript framework for building MCP servers. ## Quick Comparison ### Framework Comparison
### Cloud Platform Comparison
--- ## Creating agent tools **Gram is MCP-compatible, not MCP-specific.** When you define tools with Gram Functions, you're creating agent tools that exist independently of any protocol. These tools can be deployed as MCP servers today, but they're not locked into MCP. If another protocol becomes the standard tomorrow, whether that's code-mode execution, a different agent protocol, or something entirely new, your Gram-defined tools can be wrapped in that protocol without rewriting your core logic. **FastMCP is MCP-specific.** Tools defined with FastMCP are tightly coupled to the MCP protocol. The framework is designed as a lower-level abstraction that closely mirrors the MCP specification. This gives you direct control over MCP-specific features, but it also locks you into MCP. When the protocol evolves or you need to support a different standard, you're facing a rewrite. This distinction matters. With [ongoing discussions about MCP's utility and future direction](https://x.com/stevekrouse/status/1986922520298287496), betting your tooling infrastructure on a single protocol is a risk. Protocol-agnostic tooling lets you adapt as the ecosystem evolves without throwing away your work. ### Gram Functions vs FastMCP Framework Gram Functions (TypeScript) and FastMCP (Python) are both frameworks that abstract the MCP protocol details, but they differ in language, philosophy, and developer experience. **FastMCP is Python-specific**. You write your MCP server code in Python using FastMCP's decorator-based API. This gives you access to Python's rich ecosystem and is ideal if your team already has Python expertise. **Gram Functions currently supports TypeScript** (with Python on the roadmap). The TypeScript-first approach caters to teams working in JavaScript/TypeScript ecosystems and offers significantly less boilerplate than traditional MCP SDKs. **With FastMCP (Python):** ```python filename="my_server.py" from fastmcp import FastMCP mcp = FastMCP("My Server") @mcp.tool() async def get_order_status(order_id: str) -> dict: """Get the status of an order""" # Your implementation return await fetch_order(order_id) @mcp.tool() async def search_products(query: str, category: str = None) -> list: """Search for products""" params = {"q": query} if category: params["category"] = category return await api_client.get("/products", params=params) ``` **With Gram Functions (TypeScript):** ```typescript filename="server.ts" import { Gram } from "@gram-ai/functions"; import * as z from "zod"; const g = new Gram() .tool({ name: "get_order_status", description: "Get the status of an order", inputSchema: { orderId: z.string() }, async execute(ctx, input) { try { const response = await fetch( `https://api.example.com/orders/${input.orderId}`, { signal: ctx.signal }, ); return ctx.json(await response.json()); } catch (error) { if (error.name === "AbortError") { return ctx.fail("Request was cancelled"); } throw error; } }, }) .tool({ name: "search_products", description: "Search for products", inputSchema: { query: z.string(), category: z.string().optional(), }, async execute(ctx, input) { try { const params = new URLSearchParams({ q: input.query, ...(input.category && { category: input.category }), }); const response = await fetch( `https://api.example.com/products?${params}`, { signal: ctx.signal }, ); return ctx.json(await response.json()); } catch (error) { if (error.name === "AbortError") { return ctx.fail("Request was cancelled"); } throw error; } }, }); export const handleToolCall = g.handleToolCall; ``` Both approaches are straightforward. Gram Functions leverages Zod for schema validation, which many TypeScript developers already use. Whereas, FastMCP uses Python type hints for input validation. ### API Proxy with Automatic Sync One of the biggest differences between the platforms is how they handle API-based tools. **Gram Cloud provides true API proxy capabilities.** When you upload an OpenAPI specification, Gram generates production-ready tools that proxy requests to your API endpoints. As your API evolves, you upload an updated OpenAPI spec and your MCP tools automatically stay in sync, no code changes required. ![Update OpenAPI spec](/assets/blog/gram-vs-fastmcp-cloud/update-openapi.png) ```mermaid graph TD A[Update API Code] --> B[Regenerate OpenAPI Spec] B --> C[Upload to Gram Cloud] C --> D[MCP Tools Automatically Updated] ``` **FastMCP Cloud offers OpenAPI integration as a development starting point.** While FastMCP can generate MCP server code from your OpenAPI specification, this is primarily a bootstrapping approach. The generated code requires customization, and then you're on your own to keep your MCP server code synchronized with API changes. Every time your API evolves, you're manually updating tool definitions, testing changes, and redeploying. ```mermaid graph TD A[Existing API] --> B[Generate code stubs from OpenAPI] B --> C[Write custom MCP server code] C --> D[Configure route mappings] D --> E[Set up authentication] E --> F[Deploy to FastMCP Cloud] F --> G[MCP Server Live] H[API Changes] --> I[Update MCP server code] I --> J[Test & redeploy] J --> K[Manual sync maintenance] ``` ### Guided Tool Curation **Gram Cloud provides a UI for tool curation.** When you upload an API with 50+ endpoints, Gram helps you: 1. Select which endpoints matter for your use cases 2. Organize tools into focused toolsets 3. Enhance descriptions with business context 4. Add examples that help LLMs understand when to use each tool This guided approach helps you move from a sprawling API to a focused 5-30 tool collection that AI agents can navigate confidently. ![Gram tool curation](/assets/blog/gram-vs-fastmcp-cloud/gram-tool-curation.png) **FastMCP Cloud manages tool curation in code.** You configure route mappings, filtering, and tool organization in Python code: ```python from fastmcp import FastMCP from fastmcp.server.openapi import RouteMap, MCPType custom_routes = [ RouteMap(methods=["GET"], pattern=r"^/analytics/.*", mcp_type=MCPType.TOOL), RouteMap(pattern=r"^/admin/.*", mcp_type=MCPType.EXCLUDE), RouteMap(tags={"internal"}, mcp_type=MCPType.EXCLUDE), ] mcp = FastMCP.from_openapi( openapi_spec=openapi_spec, client=client, route_maps=custom_routes, name="My API Server" ) ``` This code-first approach gives you unlimited flexibility to build sophisticated filtering and routing logic. The downside: you're writing and maintaining curation logic yourself, debugging regex patterns, and iterating through code-test-deploy cycles every time you need to adjust which endpoints are exposed. --- ## Deploying MCP servers Once you've built your MCP server (or decided not to write code at all), you need to deploy it. The deployment model shapes how much flexibility you have down the road. ### Server Artifact vs Dynamic Composition Both platforms handle cloud hosting and infrastructure management for you, but they take fundamentally different architectural approaches. **FastMCP Cloud treats each MCP server as a single deployable artifact.** When you deploy your Python code, it runs as a dedicated application on its own infrastructure. The server process stays running, ready to handle requests. If you want to create a new MCP server with a different combination of tools, you write new code and deploy a separate server artifact. This approach mirrors traditional application deployment and works well for long-running operations. **Gram treats MCP servers as dynamic compositions of tools.** Rather than deploying code to dedicated machines, you're composing tools into dynamic toolsets. The underlying infrastructure is serverless and shared, scaling to zero when idle (sub-30ms cold starts) and automatically scaling under load. Because servers are just logical groupings rather than physical deployments, you can instantly create new "MCP servers" by composing different combinations of existing tools without writing or deploying any code. This enables the reusable toolsets capability where the same tool can participate in multiple servers simultaneously. The trade-off is that Gram's serverless model isn't suitable for operations that require extended execution time. ### Authentication and Security Both platforms support OAuth 2.1 with Dynamic Client Registration (DCR) and provide OAuth proxy capabilities for traditional providers like GitHub, Google, and Azure that don't support DCR. But the implementation experience differs significantly. **Gram Cloud provides managed authentication with UI-based configuration.** Choose between managed authentication (where Gram handles OAuth flows and token management) or passthrough authentication (where users bring their own API credentials). You configure OAuth settings through the UI, and Gram takes care of the infrastructure. The OAuth proxy runs as part of Gram's managed service—no deployment or maintenance required. ![Gram passthrough authentication](/assets/blog/gram-vs-fastmcp-cloud/gram-passthrough-auth.png) **FastMCP Cloud provides authentication building blocks you assemble in code.** Choose between remote OAuth (for DCR-enabled providers), OAuth/OIDC proxies (for traditional providers), or even build a full OAuth server yourself. You implement authentication by instantiating providers, configuring token verifiers, and setting up client storage backends in your Python code. For production deployments, you'll need to provide JWT signing keys, configure persistent storage (preferably network-accessible like Redis), and handle encryption wrapping for stored tokens. FastMCP's documentation describes the full OAuth server pattern as "an extremely advanced pattern that most users should avoid." The key difference: Gram treats authentication as a managed service you configure, while FastMCP gives you libraries to build and deploy your own authentication infrastructure. --- ## Conclusion If you're a Python shop that needs complete control over your MCP infrastructure and you're prepared to build and maintain authentication systems, FastMCP Cloud provides the libraries to construct exactly what you need. The investment is significant—both upfront and ongoing—but you get unlimited flexibility. Gram takes a different approach to the same problem. The platform is MCP-compatible rather than MCP-specific, so your tools adapt as the agent ecosystem evolves without rewrites. For code-based tools, Gram Functions provides a TypeScript framework with minimal boilerplate. For API-based tools, upload an OpenAPI spec and deploy production-ready MCP servers in minutes with automatic sync as your API evolves. Authentication, deployment, and scaling are handled for you. The question isn't which is better, it's how deep you want to go on MCP-specifically. FastMCP gives you control and the full scope of the MCP specification at the cost of significant engineering investment. Gram provides a complete platform with a deep focus on creating agent tools quickly and maintaining them easily as your product and AI ecosystem evolve. Try Gram for free today at [getgram.ai](https://app.getgram.ai). # hathora-gaming-devex Source: https://speakeasy.com/blog/hathora-gaming-devex If you're wondering why on earth Speakeasy has Unity plugins as a generation target, you can thank me and one of our early customers, [Hathora](http://hathora.dev). Before joining Speakeasy, I had a long career in the games industry, working on backends for multiplayer games. One of our goals at Speakeasy is to make a great developer experience possible, no matter what vertical a developer works in. I've pushed hard for Gaming to be one of the verticals we focus on because of the industry's proud history of innovation in the dev tooling space and because we're in a period of profound change within gaming. The development of tools like Hathora and Speakeasy is helping to establish a higher benchmark for developer experience and democratizing access to tooling that makes it possible. In this post, I will walk through some of my war stories, look at how the industry has changed over the last 15 years, and how tools like Hathora and Speakeasy catalyze another wave of change. ## Building Gaming Infrastructure the Old Way In the 2000s, connecting millions of gamers across the globe was a monumental engineering undertaking reserved for only the most successful studios with the biggest budgets. Behind every multiplayer game was an army of gaming infrastructure professionals wielding massive budgets to build a platform capable of supporting their players all across the globe. I was one such engineer working on projects as varied as the wave of multiplayer mobile games, MMORPGs, and AAA games like Halo and Gears of War. My teams were generally tasked with the creation of all the core components of the multiplayer platform: 1. Account Management - Creation and synchronization of accounts across all platforms and devices. 2. Game Synchronization - Netcode to ensure that all players would see the same game state at the same time, which meant managing differences in individual network speeds and latencies. 3. Matchmaking - Creating a system that matched players together based on variables such as skill level, geographical location, and latency. 4. Games Server Orchestration - The biggest piece of the puzzle, managing the servers that hosted the games themselves. For mass multiplayer games this included elements such as: - Load balancing - Distributing player traffic across multiple servers to ensure that no single server becomes overloaded, accounting for regional latency. - Game lifecycle management - Ensuring that games were started and stopped as needed, and that players were matched to the right game. - Fleet management - Ensuring that there were enough servers to handle the load of players. Scaling up and down as needed. Here's a simplified diagram of the multiplayer infrastructure we would've been building in the 2000s that would have been typical for a game like Halo or Gears of War: ![Image of Multiplayer Infratructure](/assets/blog/hathora-gaming-devex/hathora-infra-1.png) At every studio, resources were sunk into building similar systems and trying to figure out cost-effective hosting options. Cloud technology was just entering the picture, so for a lot of games, servers were tucked away in the back corner of the studio office until you could find dedicated locations for them. Some studios could afford hosting partners to cover the US and Europe. But that still meant players from other continents were out of luck (growing up in Cairns, Australia, my >200ms latency was painful). Needless to say, but building this infrastructure was a huge drain on resources and time that could have been spent on the actual game. ## The Standard Game Dev Technology Stack Fortunately, in the last ten years, the secular trend that reduced the cost of things like web hosting has finally started to move the needle in gaming. Services like: account management, cloud saving, cross-region networking are all now available to buy off the shelf. The catalyst has been the consolidation around a common technology stack for games: - Cloud Compute: The rise of the cloud was certainly a significant boost for developers and gamers - ping times dropped, and studios could better manage server costs as games rose and faded in popularity. You still needed to rebuild the core components of the multiplayer platform for each game, but removing the burden of server management was a huge win. - Gaming Engines: Even more important than the cloud has been the emergence of gaming engines as platforms to build on, Unity for mobile games & Unreal for PC/consoles. This consolidation has made the whole industry more efficient. Instead of reinventing the wheel, studios can focus on differentiating their gameplay experience. But even more exciting than the productivity boost realized by these developments is what it sets the table for… An explosion in tooling for game developers that is about to reshape the industry. ## The New Age of Building Multiplayer Games With consolidation around common primitives, it's now more feasible to build game development platforms (server hosting, matchmaking, live ops) that are instantly usable by the industry as a whole. This previously wasn't realistic because every studio's environment was so unique and filled with custom code that integration was impossible. Hathora, one of Speakeasy's earliest customers, is one such platform. They offer an API that makes deploying and scaling multiplayer servers worldwide a realistic weekend project. Reading through their site was equal parts exciting and frustrating. If Hathora had been an option when I was in gaming, they could have replaced entire pieces of infrastructure that were being maintained by my team. Those efforts could have instead been reinvested into the actual gameplay. Having a service provider completely changes the cost calculation of building a multiplayer game. The same goes for the other pieces of the multiplayer stack: | Feature Category | Description | Platforms | |-------------------------------|---------------------------------------|------------------------------------| | Account Management | Creation and synchronization of accounts across all platforms and devices | [LootLocker](https://lootlocker.com/), [Pragma](https://pragma.gg/), [Beamable](https://beamable.com/), [PlayFab](https://playfab.com/) | | Game Synchronization | Ensure that all players would see the same game state at the same time, managing network speeds and latencies | [Photon](https://doc.photonengine.com/pun/current/gameplay/synchronization-and-state#), [Heroic Labs](https://heroiclabs.com/), [Mirror](https://mirror-networking.com/) | | Matchmaking | Creating a system that matched players based on skill level, geographical location, and latency | [Beamable](https://beamable.com/), [Pragma](https://pragma.gg/), [Playfab](https://playfab.com/), [IDEM](https://www.idem.gg/), [Heroic Labs](https://heroiclabs.com/) | | Games Server Orchestration | Load balancing, game lifecycle management, and fleet management | [Hathora](https://hathora.dev/), [Multiplay](https://unity.com/products/game-server-hosting) | Here's what my multiplayer architecture would look like in 2024. Much simpler: ![Image of Multiplayer Infratructure with Hathora](/assets/blog/hathora-gaming-devex/hathora-infra-2.png) The availability of off-the-shelf services is going to trigger an explosion in creativity as smaller and smaller studios can execute increasingly grand visions. However, there's still work to be done to make platforms like Hathora as impactful as possible. That's where my work at Speakeasy comes in. ## The Role of API Integration Experience in Game Dev You can build the most incredible tool in the world, but if it's not easy to integrate with, it's not useful. And that's what Speakeasy is working to solve. We want it to be trivially easy to integrate with every game development platform's API. To make that happen, we're building our code gen platform to support library creation for every popular language in the game development space. We've started with C# & Unity and are planning support for Unreal and C++. We make library creation easy by using an OpenAPI spec as the only required input. Maintain a spec, and you'll get a published library ready to put in the hands of your users without additional work. Providing libraries will remove the need for your users to write the tedious boilerplate code required to connect with your API platform: auth, retries, pagination, type checking, everything is taken care of by the library. API integrations that use a library are on average 60% faster and require less maintenance over time. That allows the gaming platforms to stay focused on developing their core functionality without having to worry about the last mile of integration into their user's code base. Here's what integration with Hathora looks like with Speakeasy in the mix. The amount of code required to complete the integration is dramatically reduced: ![Image of Multiplayer Infratructure with Speakeasy](/assets/blog/hathora-gaming-devex/hathora-infra-3.png) ## The Future of Gaming APIs We're really excited to be doing our part to help the growth of APIs in gaming development. And gaming platforms are just the start. Increasingly, Public APIs are an important part of the games themselves. An API platform enables communities to expand and build upon their favorite games. Community building opens up a new world of viral opportunities: social interactions, continuous updates, and customized experiences. This will only help increase engagement and attract more players. The future for APIs in gaming is full of possibilities! # hiring-a-founding-team Source: https://speakeasy.com/blog/hiring-a-founding-team One of the most crucial yet challenging activities for an early software startup is hiring the founding engineering team. If you follow the standard hiring process for engineers, you'll gauge technical competency but [fail to assess the vital attributes of a founding engineer:](https://blog.southparkcommons.com/startup-engineering-hiring-anti-patterns/) comfort with ambiguity, strong product instincts, and a sense of ownership/agency. After some early struggles with building the Speakeasy team, we stumbled onto a solution that had been hidden in plain sight. It was the process by which I myself had come to join Speakeasy as the first engineer – **a week-long trial period as a full member of the team.** ## The Need For a New Method Initially, it was easy to default to our collective experience of being on both sides of the interview process at larger engineering organizations. This resulted in assessing devs for “competency” by sampling their abilities through coding/system design exercises. The problem we identified with this approach was that it often failed to produce strong signals. Surprisingly, this was true in many instances _regardless_ of interview performance. As an early company, the whole team would have a round table discussion for each applicant we interviewed. But even upon the debrief of a compelling candidate who had performed well on our assessments, we were often left with the feeling of, “now what?” Even if we felt certain in the candidate's technical ability, we lacked the comprehensive signal required to determine whether or not the candidate was someone who would meaningfully move the company forward. The questions we asked were too focused on what people “can do” and relegated us to assessing “how” they did it. What we needed to know was what they “will do” as an integral part of our team. Each founding engineer uniquely shapes the product and has an outsized impact on the success of the company, but we weren't able to glean that from our interviews. After reflecting on how strongly we'd felt about one another when I'd joined the company, we realized it was on us to create a similar environment for the candidates that better allowed them to showcase their strengths and make a more informed opinion about what it would be like to work with us. In turn, we would be able to develop conviction, champion candidates, and cultivate a more purposeful culture around hiring. ## My Journey to Speakeasy When I first met Sagar and Simon, I was [exploring EdTech entrepreneurship as a South Park Commons member](https://www.southparkcommons.com/community). During my exploration, I came across a classic research paper in education by Benjamin Bloom – [The 2 Sigma Problem](https://web.mit.edu/5.95/www/readings/bloom-two-sigma.pdf). Bloom found that the efficacy of 1:1 tutoring is remarkable. The _average_ tutored student performed above 98% of the students (2 standard deviations) in a control class of 30 students and 1 teacher. There's no shortage of takeaways that can be drawn from this study, but I took this as evidence for the power of focused attention in driving exceptional outcomes and the impact of [doing things that don't scale](http://paulgraham.com/ds.html) wherever possible. I decided to apply this learning to my personal career journey. The standard engineering job search typically involves lining up interviews with as many companies as possible and using competing offers as leverage to settle on the best outcome. I figured I'd be better served focusing on a single prospect at a time, diving into the subject area, and getting a more dynamic signal about the team. I wanted to build conviction deliberately. When I connected with Sagar (Speakeasy CEO), I found someone with the same philosophy; we agreed to spend a week closely collaborating together before any longer-term decisions were made. Over the span of a week, I was involved in design sessions, user discovery calls, and even interviews with other candidates. I was given an unfiltered view of the product direction with full access to all existing Notion docs, code, and proposed GTM planning. Equipped with this knowledge, I was encouraged to define and drive my own project that would be useful to the company. I came to understand the founders as not only congenial people who were easy to share a meal with, but insightful pragmatists who led with a sense of focus and approachability. I was able to identify where I could immediately contribute and where there existed proximal zones of development for my own growth. My understanding of what working at Speakeasy entailed was two standard deviations above what it typically had been prior to making previous career decisions. And I was paid for all of it. ## How it Worked in Practice We were initially unsure whether it was realistic to ask candidates to do trial periods with us, but we found them to be very open to the process. When we explained why we thought it was important, let them know we would compensate them for their time, and committed to accommodating their normal work schedules, people were eager to give working with us a shot. During their trial periods, candidates operate as a full member of the team. They are given a real project to work on, welcome to join all standups and design sessions, and are given full access to our team documents. This gives candidates the same opportunity I had to explore and build conviction in Speakeasy's mission and plan of execution. So far we've extended this process to every full time dev on the team and the results have been nothing short of stellar. The developers we've been fortunate to hire have used their collaboration periods to demonstrate ownership, thoughtfulness about the user experience, and a sincerity that is present in the quality of their work. By the time they officially joined, they had already become part of the team and made meaningful contributions. We've also observed the positive impact on team culture the collaboration period has brought. While there's value in a company having a unifying culture, people are diverse in their strengths and we believe this should be leveraged. With trial periods, we ended up taking responsibility for cultivating fit, rather than seeking it as a natural occurrence gleaned in the cumulative 4 hrs of a typical interview process. ## Thoughts on Scaling The conclusion of Bloom's “2 Sigma Problem” isn't that everyone should be tutored 1:1 (that's ideal, but totally unrealistic), rather it is an invitation to explore how we can “find methods of group instruction as effective as one-to-one tutoring”. In the same vein, we know it's not realistic to offer an extended ‘trial period' to every compelling candidate indefinitely. There will soon come a point where we need to graduate from this process, extract the core values, and develop new processes that embody these values as the company scales. It's something we're already thinking about. We believe the trial period incorporates a few values that have served us well such as radical transparency and openness. We're in the process of formally defining these values, so we can continue to be intentional in our hiring process and build a strong foundation as a team. In “The 2 Sigma Problem” there is another experiment where students within an _entire class_ performed a standard deviation better than others. The result came from shifting the responsibility to the educator to employ more tailored instructional and feedback-driven strategies. For Speakeasy, this means not throwing a bog-standard ‘Cracking the Coding Interview' question at an applicant and calling it a day. Even at scale, we'll always have a responsibility to engage with each candidate in a way that puts them at the center of the process. In an industry where many developers feel like proficiency with a leetcode question bank is often the be-all and end-all to employability, we're committed to being thoughtful about the overall candidate experience and giving them the opportunity to make genuine contributions during the hiring process. Whether this involves dynamically responding to a developer's skills as we add more structure to our process or using OSS work for collaboration that can also help build up their portfolio, we're excited to continue making the experience a positive one for all who are involved. # how-to-build-sdks Source: https://speakeasy.com/blog/how-to-build-sdks import { Callout, Table } from "@/mdx/components"; ## Why SDKs Matter for Developer Adoption An SDK can be a game-changer for accelerating developer adoption and streamlining integration. Without one, developers are left to decipher API documentation that might lack essential information or be disorganized, and then build each API call by hand. This means handling everything from authentication and request construction to response parsing, error handling, retries, pagination, and beyond—a process that significantly complicates and delays integration. In contrast, a well-designed SDK lets developers get started in minutes by importing a single library. Features like in-IDE auto-completion and type safety prevent integration bugs before they happen. Advanced tasks like rate limiting or pagination become seamless. The result is a smooth, even delightful developer experience that fosters trust in your API. Established API-first companies—Stripe, Plaid, Twilio—prove the point. Their client SDKs provide an A+ developer experience, helping them dominate in their respective markets. ## Developer Experience Without vs. With an SDK The value of an SDK becomes crystal clear when you compare the integration process with and without one. Let's contrast the developer experience: ```typescript filename="Manual API Integration" // Without SDK - Complex manual implementation async function getUser(userId: string): Promise { try { const response = await fetch(`https://api.example.com/v1/users/${userId}`, { headers: { Authorization: `Bearer ${apiKey}`, "Content-Type": "application/json", }, }); if (!response.ok) { if (response.status === 429) { // Implement rate limiting logic await sleep(calculateBackoff(response.headers)); return getUser(userId); } throw new Error(`API error: ${response.status}`); } const data = await response.json(); // Manual type validation if (!isValidUser(data)) { // Check for required fields, data types, etc. throw new Error("Invalid user data received"); } return data; } catch (error) { // Complex error handling if (shouldRetry(error)) { // e.g., check for network errors await delay(calculateBackoff()); // Exponential backoff strategy return getUser(userId); } throw error; } } // Implementing pagination async function listUsers(): Promise { const allUsers = []; let page = 1; while (true) { const response = await fetch( `https://api.example.com/v1/users?page=${page}`, { headers: { Authorization: `Bearer ${apiKey}` } }, ); const data = await response.json(); allUsers.push(...data.users); if (!data.hasNextPage) break; page++; } return allUsers; } ``` With a well-designed SDK, however, developers can get started in minutes by simply importing the library: ```typescript filename="SDK Integration" // With SDK - Clean, type-safe interface const client = new APIClient({ apiKey: process.env.API_KEY, }); // Simple request with built-in error handling const user = await client.users.get(userId); // Automatic pagination handling for await (const user of client.users.list()) { console.log(user.name); } ``` The difference is dramatic. An SDK transforms complex, error-prone implementation details into simple, intuitive method calls. It handles the heavy lifting of authentication, error handling, rate limiting, and pagination behind the scenes, allowing developers to focus on building their applications rather than wrestling with API integration details. To dive deeper into why SDKs are crucial for API adoption and developer experience, [check out our comprehensive guide on APIs vs SDKs.](/post/apis-vs-sdks-difference) ## Comparing SDK Development Options When it comes to building SDKs for your API, you have three main routes. Each comes with specific costs, maintenance burdens, and control levels:
### Hand-Written SDKs Building SDKs by hand offers maximum control over the developer experience and allows for perfect alignment with internal coding styles. However, this approach comes with significant trade-offs. The initial development cost is high (around $90,000 per SDK), and ongoing maintenance requires a dedicated team with expertise in multiple programming languages. Beyond the direct costs, several common pitfalls can undermine the success of hand-written SDKs: 1. **Neglected Maintenance:** APIs evolve. If your SDK doesn't evolve alongside it, you'll introduce breaking changes, outdated docs, and dissatisfied users. 2. **Inconsistent Language Support:** If you release multiple SDKs but treat one as a “primary” language, users in other languages may feel neglected. 3. **Improper Error Handling:** Failing to handle retries, rate limits, or authentication seamlessly can turn a developer's first impression sour. 4. **Over-Engineering or Under-Engineering:** Trying to build a “perfect” SDK can lead to months of development. Conversely, a bare-bones approach can frustrate users with missing features. 5. **Incomplete Documentation:** Even the best SDK is useless if developers can't find clear examples and usage instructions. --- ### Using OpenAPI Generator OpenAPI Generator is a robust, open-source tool that automates the creation of SDKs from your OpenAPI specifications. Distributed under the Apache 2.0 license, it offers the flexibility to modify and integrate the generated code into commercial projects across multiple languages. Its straightforward setup and integration with CI/CD pipelines make it an attractive option for teams focused on rapid prototyping or projects with narrowly defined requirements. That said, while the tool boasts a low upfront cost, it comes with a set of challenges that can become significant in production environments. For instance, as of January 2024, the repository was burdened with over 4,500 open GitHub issues—a stark indicator of the ongoing community maintenance challenges and the absence of formal support or SLAs. This reliance on community-driven fixes can result in unpredictable resolution times for critical bugs or regressions, making it a less-than-ideal choice for enterprise applications. Moreover, the quality and idiomatic correctness of the generated code can vary widely between target languages. Developers may find themselves investing considerable time customizing templates, patching bugs, and integrating advanced features like OAuth flows, rate limiting, or webhooks to meet internal standards and production-grade requirements. This hidden engineering overhead can quickly erode the initial cost savings offered by the tool. While OpenAPI Generator remains a viable solution for projects with very specific needs or constraints—particularly where time-to-market is paramount—its challenges in maintenance, support, and consistency often prompt larger organizations to consider more comprehensive, enterprise-grade alternatives. For a deeper dive into these challenges and a detailed comparison with Speakeasy's managed approach, [read our analysis.](/post/speakeasy-vs-openapi-generator) --- ### Speakeasy Speakeasy generates enterprise-grade SDKs directly from your OpenAPI spec, eliminating the need for a proprietary DSL or additional config files. **Key Benefits**: - **Fully Managed & Always Up-to-Date:** Automated validation, code generation, publishing, and regeneration upon API spec changes. - **OpenAPI-Native & No Lock-In:** Uses your existing OpenAPI spec as the single source of truth. - **Idiomatic & Enterprise-Ready:** Generates code that feels native to each language, with built-in authentication, retries, pagination, and error handling. - **Automated Testing & Documentation:** Ensures SDK compatibility and provides comprehensive API references. - **Dedicated Support:** A professional team to handle fixes, new features, and improvements. "Finding Speakeasy has been transformational in terms of our team's velocity. We've been able to progress our roadmap faster than we thought possible. We were able to get SDKs for 3 languages in production in a couple of weeks!" — Codat Engineering Team --- ## Making the Right Choice Your decision hinges on **resources, timeline,** and **feature demands**: - **Hand-Written SDKs:** Maximum control, but at the highest cost and longest development time. - **OpenAPI Generator:** A seemingly free option, but often requires significant hidden engineering effort for maintenance and customization. - **Speakeasy:** The fastest and most cost-effective way to get enterprise-grade, fully-managed SDKs, directly from your OpenAPI spec. Building SDKs is a significant investment in your API's success, but the right approach can dramatically impact both your costs and adoption rates. Speakeasy offers a compelling alternative to traditional methods, providing enterprise-grade SDKs without the usual time and cost burden. Because Speakeasy is OpenAPI-native, you maintain complete control of your API definition. Generate your first SDK in minutes using your existing OpenAPI spec, or work with our team to optimize it for the best possible results. [Click here](https://app.speakeasy.com/) to generate your first SDKs — for free. # install terraform: Source: https://speakeasy.com/blog/how-to-build-terraform-providers HashiCorp (creators of Terraform) recently released a tool for automatically creating Terraform provider data types from your OpenAPI documents. In this post we'll do a full walkthrough of how to use both of these tools, but before getting into the details, here's a high level summary of the differences between the two: - Requires a separate, custom generation configuration. Speakeasy uses the built-in OpenAPI Specification extensions mechanism to define Terraform Provider resources and data modeling. (Both must be handwritten today, but we could make a product decision to enable better UX.) - Only creates Terraform schema and data handling types. There is no code generation for the underlying code to handle API requests/responses, implement API security, nor Terraform Provider resource logic. Speakeasy does everything (works out of the box). - Does not support the full OpenAPI Specification data handling system. Speakeasy supports OAS schemas including advanced features, such as `oneOf` and `allOf`. - Has only received dependency maintenance updates since January 2024. Speakeasy is actively maintained and enhanced. - Because the Speakeasy workflow is end to end, it is easy to fully automate the generation. Speakeasy has a Github Action that can easily be added to your CI/CD. Hashicorp would require a custom script to pull together a couple of tools in order to fully automate the generation. - In contrast to Speakeasy's "OpenAPI-first" philosophy, HashiCorp uses an intermediate format between the OpenAPI schema and the provider. This format is theoretically useful if you want to create a provider with something other than OpenAPI, such as directly from your API codebase. But in that case, you probably would need to write a custom script, as there is no tool for this yet. For those who need it, let's start with a brief overview of Terraform providers and how they work. Otherwise, you can skip ahead to [Generating a Terraform Provider](#generating-a-terraform-provider) ## What Is a Terraform Provider? Terraform is a free tool to manage infrastructure with configuration files. For instance, you can call Terraform to create identical AWS servers ready to run an application in development, test, and production environments by giving it the same configuration file for each environment. A Terraform provider is a plugin to the Terraform core software that allows Terraform to access resources that live in a 3rd party system (for example, the AWS provider allows access to AWS, the system that manages cloud infrastructure resources). ## Why Build a Terraform Provider for an API? If your API calls a stateless service (to get a share price, for instance, or charge a credit card), there is nothing for configuration files to manage. However, if your API manages any resource that persists (like user permissions or an accounting system), you could offer your users a custom Terraform provider as an interface to your API, allowing them to call your service through a standard set of configuration files. For a more thorough conversation of Terraform providers and examples, see our article on the [benefits of Terraform providers](/post/build-terraform-providers). ## About Terraform Providers Terraform providers are modules written in Go as plugins for Terraform. When you install a Terraform provider with `go install`, it is saved to the `go` path, for example, `root/go/bin`. The Terraform provider filename will match the first line in the module's `go.mod` file. For example, the following: ```go module github.com/speakeasy/terraform-provider-terraform ``` Will match the file `root/go/bin/terraform-provider-terraform`. However, the name you use for the provider in the `source` field of the Terraform resource configuration file comes from your module's `main.go` file. In this excerpt: ```hcl terraform { required_providers { terraform = { source = "speakeasy/terraform" version = "0.0.3" } } } ``` The name used in the `source` field comes from the module's `main.go` file: ```go opts := providerserver.ServeOpts{ Address: "registry.terraform.io/speakeasy/terraform", ``` When building and testing Speakeasy and HashiCorp providers locally, you need to match your Terraform repository overrides to the `main.go` field in `.terraformrc`: ```hcl provider_installation { dev_overrides { "speakeasy/terraform" = "/root/go/bin" "terraform-provider-petstore" = "/root/go/bin" } direct {} } ``` ## Generating a Terraform Provider ### Prerequisites If you would like to create a Terraform provider by following this tutorial, you'll need [Docker](https://docs.docker.com/get-docker) installed. Alternatively, you can install Go, Node.js, Speakeasy, Terraform, and the Terraform provider creation modules on your local machine. Below is a Dockerfile that creates an image prepared with Go, Terraform, Speakeasy, and the new HashiCorp codegen framework that you can use to run all the code in this article. Replace the Speakeasy key with your own on line nine and save the file as `Dockerfile`. ```bash FROM --platform=linux/amd64 alpine:3.19 WORKDIR /workspace RUN apk add go curl unzip bash sudo nodejs npm vim ENV GOPATH=/root/go ENV PATH=$PATH:$GOPATH/bin ENV SPEAKEASY_API_KEY=SET_YOUR_API_KEY_HERE RUN curl -O https://releases.hashicorp.com/terraform/1.7.0/terraform_1.7.0_linux_amd64.zip && \ unzip terraform_1.7.0_linux_amd64.zip && \ mv terraform /usr/local/bin/ && \ rm terraform_1.7.0_linux_amd64.zip # install openapi terraform provider framework: RUN go install github.com/hashicorp/terraform-plugin-codegen-framework/cmd/tfplugingen-framework@latest RUN go install github.com/hashicorp/terraform-plugin-codegen-openapi/cmd/tfplugingen-openapi@latest # install speakeasy RUN curl -fsSL https://go.speakeasy.com/cli-install.sh | sh ``` Run the following commands in a terminal to start using the Dockerfile: ```sh mkdir code docker build -t seimage . # build the image docker run -it --volume ./code:/workspace --name sebox seimage # run the container using the code folder to work in # Run this command if you need to start the container again later: # docker start -ai sebox ``` In another terminal on your host machine (not inside Docker), run the code below to create folders to work in for the Speakeasy and HashiCorp examples: ```sh cd code mkdir hashi # hashicorp mkdir se # speakeasy cd se touch openapi.yaml ``` You need to give your host user write permissions to files created in the container to edit them. On Unix-like systems, use the following command on your host machine, replacing `myusername` with your user name: ``` sudo chown -R myusername:myusername code ``` Now insert the Swagger Petstore example schema (available [here](https://github.com/speakeasy-api/examples-repo/blob/main/how-to-build-terraform-providers/original-openapi.yaml)) into the `openapi.yaml` file. The Swagger Petstore example schema is the API description of a service that manages pets and orders for pets at a store. ## Create a Terraform Provider With the HashiCorp Provider Spec Generator The [HashiCorp automated Terraform provider creator](https://developer.hashicorp.com/terraform/plugin/code-generation/design) works differently from Speakeasy. HashiCorp provides an API SDK to convert your OpenAPI schema into an intermediate format, called the provider code specification (JSON). This document is then transformed into a provider by Terraform SDK. These transforming SDKs are both CLI tools. ```mermaid flowchart TD subgraph 0[" "] 1(📜 OpenAPI schema) 1b(📜 Generator configuration) end 0 --⚙️ API SDK--> 2(📃 Provider code specification) 2 --⚙️ Terraform SDK --> 3(🗝️ Go provider) ``` An OpenAPI schema does not describe which Terraform resource each operation maps to. So, like Speakeasy extension attributes, you need to include mapping information in a generator configuration file along with your schema. You can create a provider by starting at any one of the three steps: - [Start with an OpenAPI schema](https://developer.hashicorp.com/terraform/plugin/code-generation/openapi-generator), as with Speakeasy. - [Write a provider code specification manually](https://developer.hashicorp.com/terraform/plugin/code-generation/framework-generator), or with some tool the Terraform community may develop in the future, without using OpenAPI at all. Writing a provider code specification is closely coupled to the Terraform system and allows you to precisely create a compatible provider. You can even include custom Go functions to map objects between your API and Terraform. The full list of features with examples is [here](https://developer.hashicorp.com/terraform/plugin/code-generation/specification). - Create a provider manually by coding it in Go (the traditional way). As noted in the [HashiCorp code generation design principles](https://developer.hashicorp.com/terraform/plugin/code-generation/design#apis-support-more-than-just-terraform-providers), there is a mismatch between an OpenAPI schema and a Terraform provider. Providers expect resources, transfers, and errors to relate in a way that an API doesn't. For this reason, it's unlikely that there will ever be a general solution to creating a provider from a schema that does not require annotations like Speakeasy extension attributes or a HashiCorp generator configuration. ### The HashiCorp Workflow Example HashiCorp provides a [full walkthrough for creating a Terraform provider from a schema](https://developer.hashicorp.com/terraform/plugin/code-generation/workflow-example). The Docker container you have been working in has everything you need to follow the Hashicorp walkthrough if you'd like to. Change to the `hashi` folder and continue working. We don't repeat HashiCorp's tutorial here, but let's take a look at the steps and how the process differs from Speakeasy. - HashiCorp doesn't create a full Go project for you. You need to create one with `go mod init terraform-provider-petstore`. - You need to use the framework code generator `scaffold` command to create a template for the Go provider code, and write your own `main.go` file. - Instead of adding attributes to your schema, you create a `generator_config.yml` file to hold the mapping between the schema and the provider. It looks like this: ```yaml provider: name: petstore resources: pet: create: path: /pet method: POST read: path: /pet/{petId} method: GET schema: attributes: aliases: petId: id ``` - The `tfplugingen-openapi generate` command creates a provider code specification in JSON from your schema, and then `tfplugingen-framework generate` creates the provider from the provider code specification, like so: ```sh tfplugingen-openapi generate \ --config ./generator_config.yaml \ --output ./specification.json \ ./openapi.yaml && tfplugingen-framework generate all \ --input specification.json \ --output internal/provider ``` - This creates the Go files `internal/provider/provider_petstore/petstore_provider_gen.go` and `internal/provider/resource_pet/pet_resource_gen.go`. The `_gen` in the filename is a hint that a tool created the file, and it should not be edited manually. - The HashiCorp Terraform provider generator does not create all the code you need. It creates only the Go data types for the pet resource, not the code that manages the data. In the middle of the HashiCorp Terraform provider generation walkthrough, you can see that a long page of code needs to be copied and pasted into `/internal/provider/pet_resource.go`. ## Create a Terraform Provider With Speakeasy In the Docker container terminal, navigate to the `se` directory using the command `cd /workspace/se` and run the following command to check that your OpenAPI schema is acceptable to Speakeasy: ```sh speakeasy validate openapi -s openapi.yaml; ``` The Petstore schema has three object types: pet, order, and user. To test Terraform providers, we'll implement the `addPet` and `getPetById` operations. Terraform will use these two operations to create a single pet, and then check if the pet exists when verifying the Terraform state against the API. These operations correspond to Create and Read in CRUD. ### Add Speakeasy Terraform Annotations We use Speakeasy extension attributes in a schema to have Speakeasy create a Terraform provider. The Speakeasy extensions tell Terraform which OpenAPI schema operations map to which CRUD operations for each resource, and which field is the ID. Read our [overview of how the Terraform provider creation process works](/docs/create-terraform) or view the [full list of Speakeasy Terraform extensions](/docs/terraform/customize) for more information about creating Terraform providers with Speakeasy. Insert the following commented lines into `openapi.yaml`: ```yaml ... post: tags: - pet summary: Add a new pet to the store description: Add a new pet to the store x-speakeasy-entity-operation: Pet#create # <----- operationId: addPet ... /pet/{petId}: get: tags: - pet summary: Find pet by ID description: Returns a single pet operationId: getPetById x-speakeasy-entity-operation: Pet#read # <----- parameters: - name: petId x-speakeasy-match: id # <----- ... components: schemas: ... Pet: x-speakeasy-entity: Pet # <----- required: - name - photoUrls type: object ... ``` Validate the schema and create the provider in the Docker container: ```sh speakeasy validate openapi -s openapi.yaml && speakeasy quickstart ``` Speakeasy creates a Terraform provider (Go module) in the `sdk` folder. #### Speakeasy Security Support In most cases, Speakeasy should support [security in providers](/docs/create-terraform): > Every authentication mechanism that relies on static authorization is supported with its values automatically available to be configured in the provider configuration. But you may encounter instances where this will not be true in practice. If you create a provider for the Petstore schema without removing the security elements, when you run `go run main.go --debug`, you'll get the error: ```sh internal/provider/pet_data_source.go:135:43: not enough arguments in call to r.client.Pet.GetPetByID have (context.Context, operations.GetPetByIDRequest) want (context.Context, operations.GetPetByIDRequest, operations.GetPetByIDSecurity) ``` In this instance, Speakeasy will not create a provider, as the API method is defined to have differing *resource* security configuration to the global *provider* security configuration in the OpenAPI specification. Similarly, for oAuth2 authenticated APIs, the authentication mechanism between a Client and a Token endpoint is ambiguous, even in the (latest) OpenAPI 3.1 Specification. Speakeasy currently natively supports the `client_secret_post` oAuth2 authentication schema as described in Section 9 of [OpenID Connect Core 1.0](https://web.archive.org/web/20250916171023/https://openid.net/specs/openid-connect-discovery-1_0.html#OpenID.Core), under the client credentials flow, but does require that some code is written to help authenticate your provider with your service for your flavour of authentication. ### Install the Terraform Provider Run the commands below in the Docker terminal to install the Terraform provider as `root/go/bin/terraform-provider-terraform`: ```sh cd sdk go install ``` You need to edit the Terraform settings file to redirect provider requests from the online Terraform repository to the custom local provider we created, or Terraform will try to find a provider online for your service and fail. The file is not in the shared `code` folder, so you need to edit it in the Docker terminal. Below we use Vim. ```sh vim /root/.terraformrc # inside vim: # i to enter into edit mode # ctrl-shift-v to paste the text below: provider_installation { dev_overrides { "speakeasy/terraform" = "/root/go/bin", "terraform-provider-petstore" = "/root/go/bin" } direct {} } # escape to exit edit mode # :wq to write and quit vim ``` Here we add `terraform-provider-petstore` in addition to the Speakeasy line so that your container is ready to run the HashiCorp tutorial later. ### Call the Provider From Terraform Now that the provider is installed and ready, we need a Terraform resource configuration file as input. Insert the code from `sdk/examples/provider/provider.tf` (which defines the provider to use) into `sdk/examples/resources/terraform_pet/resource.tf` (which defines the resource to change). The final `sdk/examples/resources/terraform_pet/resource.tf` file should look like this: ```hcl terraform { required_providers { terraform = { source = "speakeasy/terraform" version = "0.0.1" } } } provider "terraform" { # Configuration options } resource "terraform_pet" "my_pet" { id = 10 name = "doggie" photo_urls = [ "...", ] status = "available" } ``` Now we can call the Petstore service through the API using Terraform and the resource configuration file. In Docker: ```sh cd /workspace/se/sdk/examples/resources/terraform_pet terraform plan terraform apply ``` The result is an expected Terraform execution: ```sh /workspace/se/sdk/examples/resources/terraform_pet # terraform apply ╷ │ Warning: Provider development overrides are in effect │ │ The following provider development overrides are set in the CLI configuration: │ - speakeasy/terraform in /root/go/bin │ - speakeasy/hashicups in /root/go/bin │ │ The behavior may therefore not match any released version of the provider and applying changes may cause the state to become incompatible with published releases. ╵ Terraform used the selected providers to generate the following execution plan. Resource actions are indicated with the following symbols: + create Terraform will perform the following actions: # terraform_pet.my_pet will be created + resource "terraform_pet" "my_pet" { + category = (known after apply) + id = 10 + name = "doggie" + photo_urls = [ + "...", ] + status = "available" + tags = (known after apply) } Plan: 1 to add, 0 to change, 0 to destroy. Do you want to perform these actions? Terraform will perform the actions described above. Only 'yes' will be accepted to approve. Enter a value: yes terraform_pet.my_pet: Creating... terraform_pet.my_pet: Creation complete after 2s [name=doggie] Apply complete! Resources: 1 added, 0 changed, 0 destroyed. ``` You'll notice we do not have a local implementation of the API service running somewhere for the provider to call. The Petstore example is a real service provided online by Swagger. You can see the URL in the `server` field of the schema. For example, you can browse to an operation at https://petstore3.swagger.io/. If you are building your own API, you'll need to make a service the provider can call. ### Summary of the Speakeasy Terraform Provider Creation Process Let's review what we did: - Annotate an OpenAPI schema file with Speakeasy extension attributes to indicate which operations are for which Terraform CRUD functions. - Remove security elements and non-JSON request and response elements. - Create the Terraform provider Go code with Speakeasy. - Compile and install the provider. - Redirect Terraform settings to use the local provider. - Make a Terraform resource configuration file. - Run Terraform against the file. As your API changes over time and you release new Terraform provider versions to clients, you will need to add extension attributes. Apart from that, the Terraform provider creation process should be an automated set of terminal commands. ## Comparison and Conclusion In conclusion, Speakeasy provides a complete solution for creating a Terraform provider from a schema with only one manual step. The HashiCorp automated Terraform provider generator is not ready for production use yet, but is something to keep an eye on during 2024 to see how it catches up. The HashiCorp provider code specification intermediate language will also benefit from the massive Terraform community building plugins. Read [our case study on how leading data integration platform Airbyte uses Speakeasy](/customers/airbyte) to create and maintain Terraform providers alongside SDKs and minimize engineer burden. ## Further Reading - [Benefits of providers for an API](/post/build-terraform-providers) - [Original Petstore example](https://github.com/speakeasy-api/examples-repo/blob/main/how-to-build-terraform-providers/original-openapi.yaml) - [Annotated Petstore example](https://github.com/speakeasy-api/examples-repo/blob/main/how-to-build-terraform-providers/annotated-openapi.yaml) - [HashiCorp provider example](https://github.com/hashicorp/terraform-provider-hashicups) - [Speakeasy provider example](https://github.com/speakeasy-sdks/terraform-provider-hashicups) - [OpenTofu — the open-source Terraform substitute](https://opentofu.org) ### HashiCorp Automated Terraform Provider Creation - [Documentation overview](https://developer.hashicorp.com/terraform/plugin/code-generation/design) - [Provider code specification documentation](https://developer.hashicorp.com/terraform/plugin/code-generation/specification) - [OpenAPI schema SDK](https://github.com/hashicorp/terraform-plugin-codegen-openapi) - [Terraform SDK](https://github.com/hashicorp/terraform-plugin-codegen-framework) ### Speakeasy Automated Terraform Provider Creation - [Documentation overview](/docs/create-terraform) - [Annotation documentation](/docs/terraform/customize/entity-mapping) # ECOM SDK Source: https://speakeasy.com/blog/how-to-create-a-custom-python-sdk import { Callout } from "@/mdx/components"; This tutorial demonstrates one way to code and package a Python SDK. ## Why Are We Building an SDK? Since the 2000s, HTTP APIs have enabled businesses to be more connected than ever and, as a result, produce higher utility and profit — no wonder APIs have exploded in popularity. So, your business has created an API? Great! At first, developers might use `curl` or create a Postman collection to experiment with your API. However, the true power of an API is only unleashed when developers can interact with it in an automated and programmatic way. To get some real work done, you'll want to wrap the HTTP API in your language of choice, so that it can be testable and have the most important aspects abstracted for your convenience and productivity. ## What Are We Building? We'll build a Python SDK that wraps an HTTP API. Fictional e-commerce startup ECOM enables the selling of products through the creation of online stores and associated products on its website. The ECOM HTTP API enables developers to create stores and manage products in an automated way. We want to give Python programmers easy access to the ECOM API with a robust, secure, and user-friendly SDK that handles the underlying HTTP API service in a type-safe, programmatic way. ## Example SDK Repository You can follow along without downloading our complete example repository, but if you get stuck or want to see the final result, you can find the code on GitHub at [speakeasy-api/ecom-sdk](https://github.com/speakeasy-api/ecom-sdk). ## Initialize the Project Before we begin coding the SDK, let's set up our project. ### Install pyenv While not strictly necessary in all cases, we prefer to manage Python versions with pyenv. If you're building the SDK with us, [install `pyenv`](https://github.com/pyenv/pyenv?tab=readme-ov-file#installation) for your operating system. On macOS, you can install `pyenv` with Homebrew: ```bash brew install pyenv ``` ### Decide on a Python Version Deciding on a [Python version](https://devguide.python.org/versions/) that your SDK will support is important. At the time of writing, Python 3.8 is the minimum version supported by many popular libraries and frameworks. Python 3.12 is the latest stable version. Even though Python 2 is no longer supported, some older projects still use it, so you may need to support Python 2.7 if your SDK is to be used in internal legacy systems that have not yet been updated. For this tutorial, we'll use Python 3.8, because it is the oldest version that still receives security updates. ### Install Python 3.8 Install Python 3.8 with `pyenv`: ```bash pyenv install 3.8.19 ``` ### Set Up a Python Virtual Environment We'll set up a Python virtual environment to isolate our project dependencies from the system Python installation. Create a new Python 3.8 virtual environment: ```bash pyenv virtualenv 3.8.19 ecom-sdk ``` Activate the virtual environment: ```bash pyenv activate ecom-sdk ``` After activating the virtual environment, depending on the shell you're using, your shell prompt might change to indicate that you are now using the virtual environment. Any Python packages you install will be installed in the virtual environment and won't affect the system Python installation. ### Upgrade pip When you create a new virtual environment, it's good practice to upgrade `pip` to the latest version: ```bash python3.8 -m pip install --upgrade pip ``` ### Decide on a Build Backend The [complexities of Python packaging](https://packaging.python.org/en/latest/) can make navigating your options challenging. We will outline the options briefly before selecting an option for this particular example. In the past, Python used a `setup.py` or `setup.cfg` configuration file to prepare Python packages for distribution, but new standards (like PEPs 517, 518, 621, and 660) and build tools are modernizing the Python packaging landscape. We want to configure a modern build backend for our SDK and we have many options to choose from: - [Hatchling](https://hatch.pypa.io/latest/history/hatchling/) - [Poetry](https://python-poetry.org/) - [PDM](https://pdm-project.org/) - [Setuptools](https://pypi.org/project/setuptools/) - [Flit](https://flit.pypa.io/en/stable/) - [Maturin](https://github.com/PyO3/maturin) Some factors to consider when selecting a build backend include: - **Python version support:** Some build backends only support a subset of Python versions, so ensure your chosen build backend supports the correct versions. - **Features:** Determine the features you need and choose a backend that meets your requirements. For example, do you need features like project management tools, or package uploading and installation capabilities? - **Extension support:** Do you need support for extension modules in other languages? We'll use [Hatchling](https://hatch.pypa.io/latest/history/hatchling/) in this tutorial for its convenient defaults and ease of configurability. ### Set Up a Project With Hatchling First, install Hatch: ```bash pip install hatch ``` Now, create a new project with Hatch: ```bash hatch new -i ``` The `-i` flag tells Hatch to ask for project information interactively. Enter the following information when prompted: ```bash Project name: ECOM SDK Description: Python SDK for the ECOM HTTP API ``` Hatch will create a new project directory with the name `ecom-sdk` and initialize it with a basic `pyproject.toml` file. Change into the project directory: ```bash cd ecom-sdk ``` ### See the Project Structure Run `tree` to see the project structure: ```bash tree . ``` The project directory should now contain the following files: ```bash . ├── LICENSE.txt ├── README.md ├── pyproject.toml ├── src │ └── ecom_sdk │ ├── __about__.py │ └── __init__.py └── tests └── __init__.py 4 directories, 6 files ``` The `LICENSE.txt` file contains the MIT license, the `README.md` file has a short installation hint, and the `pyproject.toml` file contains the project metadata. ### Update the README File If you continue to support and expand this SDK, you'll want to keep the `README.md` file up to date with the latest documentation, providing installation and usage instructions, and any other information relevant to your users. Without a good README, developers won't know where to start and won't use the SDK. For now, let's add a short description of what the SDK does: ```markdown Python SDK for the ECOM HTTP API ``` ### Create a Basic SDK Add a `./src/ecom_sdk/sdk.py` file containing Python code, for example: ```python def sdkFunction(): return 1 ``` We'll use the `./src/ecom_sdk/sdk.py` file to ensure our testing setup works. ### Create a Test As we add functionality to the SDK, we will populate the `./tests` directory with tests. For now, create a `./src/ecom_sdk/test_sdk.py` file containing test code: ```python import src.ecom_sdk.sdk def test_sdk(): assert src.ecom_sdk.sdk.sdkFunction() == 1 ``` Later in this guide, we'll run the test with scripts provided by Hatch. ### Inspect the Build Backend Configuration The `pyproject.toml` file contains the project metadata and build backend configuration. Be sure to take a peek at the `pyproject.toml` [guide](https://packaging.python.org/en/latest/guides/writing-pyproject-toml/#writing-pyproject-toml) and [specification](https://packaging.python.org/en/latest/specifications/pyproject-toml/#pyproject-toml-specification) for details on all the possible metadata fields available for the `[project]` table. For example, you may want to enhance the discoverability of your SDK on PyPI by specifying keyword metadata or additional classifiers. ### Test Hatch Here's the Hatch test script that we'll use to run tests: ```bash hatch run test ``` The first time you run the test script, Hatch will install the necessary dependencies and run the tests. Subsequent runs will be faster because the dependencies are already installed. After running the test script, you should see output similar to the following: ```bash ========================= test session starts ========================== platform darwin -- Python 3.12.3, pytest-8.1.1, pluggy-1.5.0 rootdir: /speakeasy/ecom-sdk configfile: pyproject.toml collected 1 item tests/test_sdk.py . [100%] ========================== 1 passed in 0.00s =========================== ``` ## Code the SDK Now that we have the project set up, let's start coding the SDK. ### Add an SDK Method To Fetch a List of Stores In the `./src/ecom_sdk/sdk.py` file, define a class, constructor, and `list_stores()` method for fetching a list of stores from our API: ```python import requests HTTP_TIMEOUT_SECONDS = 10 class EComSDK: def __init__(self, api_url, api_key): self.api_url = api_url self.api_key = api_key def list_stores(self): r = requests.get( self.api_url + "/store", headers={"X-API-KEY": self.api_key}, timeout=HTTP_TIMEOUT_SECONDS, ) if r.status_code == 200: return r.json() else: raise Exception("Invalid response status code: " + str(r.status_code)) ``` Now we can begin writing tests in the `./tests/test_sdk.py` file to test that the newly added `list_stores()` method works as intended. In `requests.get()`, we use `HTTP_TIMEOUT_SECONDS` to set a maximum bound for waiting for the request to finish. If we don't set a timeout, the `requests` library will wait for a response forever. Add the following to `./tests/test_sdk.py`: ```python from src.ecom_sdk.sdk import EComSDK import responses from responses import matchers api_url = "https://example.com" api_key = "hunter2" def test_sdk_class(): sdk = EComSDK(api_url, api_key) assert sdk.api_url == api_url assert sdk.api_key == api_key @responses.activate def test_sdk_list_stores(): responses.add( responses.GET, api_url + "/store", json=[ {"id": 1, "name": "Lidl", "products": 10}, {"id": 2, "name": "Walmart", "products": 15}, ], status=200, match=[matchers.header_matcher({"X-API-KEY": api_key})], ) sdk = EComSDK(api_url, api_key) stores = sdk.list_stores() assert len(stores) == 2 assert stores[0]["id"] == 1 assert stores[0]["name"] == "Lidl" assert stores[1]["id"] == 2 assert stores[1]["name"] == "Walmart" ``` Here we use the `responses` library to mock API responses since we don't have a test or staging version of the ECOM HTTP API. However, even with the ECOM API, we might choose to have some part of the testing strategy mock the API responses as this approach can be faster and tightly tests code without external dependencies. The `test_sdk_class()` test function checks that we can instantiate the class and the correct values are set internally. The `test_sdk_list_stores()` test function makes a call to `sdk.list_stores()` to test that it receives the expected response, which is a JSON array of stores associated with the user's account. ### Add Dependencies We need to add the `requests` and `responses` libraries to the project dependencies. Add the following to the `pyproject.toml` file: ```toml dependencies = [ "requests", "responses", "pydantic", ] ``` ### Run the Tests Let's run the Hatch `test` script we configured earlier to check that everything is working correctly: ```bash hatch run test ``` Once Hatch has installed our new dependencies, you should see output similar to the following: ```bash =============================== test session starts ================================ platform darwin -- Python 3.12.3, pytest-8.1.1, pluggy-1.5.0 rootdir: /speakeasy/ecom-sdk configfile: pyproject.toml collected 2 items tests/test_sdk.py .. [100%] ================================ 2 passed in 0.09s ================================= ``` ## Exception Handling To create a pleasant surface for our SDK users, we'll hide details of HTTP implementation behind exception handling and provide helpful error messages should things go wrong. Let's modify the `list_stores()` function to catch some more common errors and print helpful information for the user: ```python filename="src/ecom_sdk/sdk.py" import requests HTTP_TIMEOUT_SECONDS = 10 class EComSDK: def __init__(self, api_url, api_key): self.api_url = api_url self.api_key = api_key def list_stores(self): try: r = requests.get( self.api_url + "/store", headers={"X-API-KEY": self.api_key}, timeout=HTTP_TIMEOUT_SECONDS, ) r.raise_for_status() except requests.exceptions.ConnectionError as err: raise ValueError("Connection error, check `EComSDK.api_url` is set correctly") from err except requests.exceptions.HTTPError as err: if err.response.status_code == 403: raise ValueError("Authentication error, check `EComSDK.api_key` is set correctly") from err else: raise return r.json() ``` When an exception is raised from the call to `requests.get()`, we catch the exception, add useful information, and then re-raise the exception for the SDK user to handle in their code. The call to `r.raise_for_status()` modifies the `requests` library behavior to raise an `HTTPError` if the HTTP response status code is unsuccessful. Read more about `raise_for_status()` in the [Requests library documentation](https://requests.readthedocs.io/en/latest/api/#requests.Response.raise_for_status). Now we'll add tests for a 403 response and a connection error to the `test_sdk.py` file. We'll also import the `requests` library into the `test_sdk.py` file: ```python filename="tests/test_sdk.py" import requests # ... @responses.activate def test_sdk_list_stores_connection_error(): responses.add( responses.GET, api_url + "/store", body=requests.exceptions.ConnectionError(), ) sdk = EComSDK(api_url, api_key) try: sdk.list_stores() except ValueError as err: assert "Connection error" in str(err) else: assert False, "Expected ValueError" @responses.activate def test_sdk_list_stores_403(): responses.add( responses.GET, api_url + "/store", status=403, ) sdk = EComSDK(api_url, api_key) try: sdk.list_stores() except ValueError as err: assert "Authentication error" in str(err) else: assert False, "Expected ValueError" ``` This test ensures that the helpful error message from our SDK is displayed when a connection error or 403 response occurs. Check that the new code passes the test: ```bash hatch run test ``` ## Type Safety With Enums To make development easier and less error-prone for our SDK users, we'll define Enums to use with the `list_products` endpoint. Add the following Enums to the `EComSDK` class: ```python filename="src/ecom_sdk/sdk.py" import requests from enum import Enum HTTP_TIMEOUT_SECONDS = 10 class EComSDK: # ... class ProductSort(str, Enum): PRICE = "price" QUANTITY = "quantity" class ProductSortOrder(str, Enum): DESC = "desc" ASC = "asc" # ... ``` The `ProductSort` and `ProductSortOrder` Enums inherit from the `str` class and can be used with the `list_products()` method we'll define next – `ProductSort` specifies the sort category, and `ProductSortOrder` determines the sort order of the product list. Now define the `list_products()` function at the bottom of the `EComSDK` class: ```python filename="src/ecom_sdk/sdk.py" # ... class EComSDK: # ... def list_products(self, store_id, sort_by=ProductSort.PRICE, sort_order=ProductSortOrder.ASC): try: r = requests.get( self.api_url + f"/store/{store_id}/product", headers={"X-API-KEY": self.api_key}, params={"sort_by": sort_by, "sort_order": sort_order}, timeout=HTTP_TIMEOUT_SECONDS, ) r.raise_for_status() except requests.exceptions.ConnectionError as err: raise ValueError("Connection error, check `EComSDK.api_url` is set correctly") from err except requests.exceptions.HTTPError as err: if err.response.status_code == 403: raise ValueError("Authentication error, check `EComSDK.api_key` is set correctly") from err else: raise return r.json() ``` The `list_products()` function accepts named parameters `sort_by` and `sort_order`. If the `list_products()` function is called with named parameters, the parameters are added to the `params` dictionary to be included in the request's HTTP query parameters. An SDK user can call the `list_products()` function with the string `list_products(sort_by="quantity")` or with the safer equivalent using Enums: `list_products(sort_by=sdk.ProductSort.QUANTITY)`. By encouraging the user to use Enums in this way, we prevent HTTP error requests that don't clearly identify what went wrong should a user mistype a string, for example, "prise" instead of "price". Now add another test at the bottom of the `test_sdk.py` file: ```python filename="tests/test_sdk.py" # ... @responses.activate def test_sdk_list_products_sort_by_price_desc(): store_id = 1 responses.add( responses.GET, api_url + f"/store/{store_id}/product", json=[ {"id": 1, "name": "Banana", "price": 0.5}, {"id": 2, "name": "Apple", "price": 0.3}, ], status=200, match=[matchers.header_matcher({"X-API-KEY": api_key})], ) sdk = EComSDK(api_url, api_key) products = sdk.list_products(store_id, sort_by=EComSDK.ProductSort.PRICE, sort_order=EComSDK.ProductSortOrder.DESC) assert len(products) == 2 assert products[0]["id"] == 1 assert products[0]["name"] == "Banana" assert products[1]["id"] == 2 assert products[1]["name"] == "Apple" ``` Check that the new test passes: ```bash hatch run test ``` ## Output Type Safety With Pydantic To make the SDK even more user-friendly, we can use Pydantic to create data models for the responses from the ECOM API. First, add Pydantic to the project dependencies in the `pyproject.toml` file: ```toml dependencies = [ "requests", "responses", "pydantic", ] ``` Now, create a `Product` model in the `./src/ecom_sdk/models.py` file: ```python filename="src/ecom_sdk/models.py" from pydantic import BaseModel class Product(BaseModel): id: int name: str price: float ``` Next, import the `Product` model into the `./src/ecom_sdk/sdk.py` file: ```python filename="src/ecom_sdk/sdk.py" from .models import Product ``` Modify the `list_products()` function to return a list of `Product` models: ```python filename="src/ecom_sdk/sdk.py" # ... class EComSDK: # ... def list_products(self, store_id, sort_by=ProductSort.PRICE, sort_order=ProductSortOrder.ASC): try: r = requests.get( self.api_url + f"/store/{store_id}/product", headers={"X-API-KEY": self.api_key}, params={"sort_by": sort_by, "sort_order": sort_order}, timeout=HTTP_TIMEOUT_SECONDS, ) r.raise_for_status() except requests.exceptions.ConnectionError as err: raise ValueError("Connection error, check `EComSDK.api_url` is set correctly") from err except requests.exceptions.HTTPError as err: if err.response.status_code == 403: raise ValueError("Authentication error, check `EComSDK.api_key` is set correctly") from err else: raise return [Product(**product) for product in r.json()] ``` The `list_products()` function now returns a list of `Product` models created from the JSON response. Now update the `test_sdk.py` file to test the new `Product` model: ```python filename="tests/test_sdk.py" # ... from src.ecom_sdk.models import Product # ... @responses.activate def test_sdk_list_products_sort_by_price_desc(): store_id = 1 responses.add( responses.GET, api_url + f"/store/{store_id}/product", json=[ {"id": 1, "name": "Banana", "price": 0.5}, {"id": 2, "name": "Apple", "price": 0.3}, ], status=200, match=[matchers.header_matcher({"X-API-KEY": api_key})], ) sdk = EComSDK(api_url, api_key) products = sdk.list_products(store_id, sort_by=EComSDK.ProductSort.PRICE, sort_order=EComSDK.ProductSortOrder.DESC) assert len(products) == 2 assert products[0].id == 1 assert products[0].name == "Banana" assert products[1].id == 2 assert products[1].name == "Apple" assert isinstance(products[0], Product) assert isinstance(products[1], Product) ``` We'll also need to update the `test_sdk_list_products()` test to check that the `Product` models are returned from the `list_products()` function. ```python filename="tests/test_sdk.py" # ... assert len(products) == 2 assert products[0].id == 1 assert products[0].name == "Banana" assert products[0].price == 0.5 assert products[1].id == 2 assert isinstance(products[0], Product) assert isinstance(products[1], Product) ``` Check that the new tests pass: ```bash hatch run test ``` We've now added type safety to the SDK using Pydantic, ensuring that the SDK user receives a list of `Product` models when calling the `list_products()` function. The same principle can be applied to other parts of the SDK to ensure that the user receives the correct data types. ## Add Type Safety to the SDK Constructor To ensure that the SDK user provides the correct data types when instantiating the `EComSDK` class, we can use Pydantic to create a `Config` model for the SDK constructor. First, create a `Config` model in the `./src/ecom_sdk/models.py` file: ```python filename="src/ecom_sdk/models.py" from pydantic import BaseModel class Config(BaseModel): api_url: str api_key: str ``` Next, import the `Config` model into the `./src/ecom_sdk/sdk.py` file: ```python filename="src/ecom_sdk/sdk.py" from .models import Config ``` Now, modify the `EComSDK` class to accept a `Config` model in the constructor: ```python filename="src/ecom_sdk/sdk.py" # ... class EComSDK: def __init__(self, config: Config): self.api_url = config.api_url self.api_key = config.api_key # ... ``` The `EComSDK` class now accepts a `Config` model in the constructor, ensuring that the SDK user provides the correct data types when instantiating the class. Let's update the `test_sdk.py` file to test the new `Config` model: ```python filename="tests/test_sdk.py" # ... from src.ecom_sdk.models import Config # ... def test_sdk_class(): config = Config(api_url="https://example.com", api_key="hunter2") sdk = EComSDK(config) assert sdk.api_url == config.api_url assert sdk.api_key == config.api_key ``` Follow the same pattern to update the other tests in the `test_sdk.py` file to use the new `Config` model. Replace `sdk = EComSDK(api_url, api_key)` with: ```python filename="tests/test_sdk.py" # ... config = Config(api_url=api_url, api_key=api_key) sdk = EComSDK(config) # ... ``` Check that the new tests pass: ```bash hatch run test ``` ## Things to Consider We've described some basic steps for creating a custom Python SDK, but there are many facets to an SDK project besides code that contribute to its success or failure. For any publicly available Python library, you should consider such aspects as documentation, linting, and licensing. ### Documentation To focus on the nuts and bolts of a custom Python SDK, this guide does not cover developing an SDK's documentation. But documentation is critical to ensure your users can easily pick up your library and use it productively. Without good documentation, developers may opt not to use your SDK at all. All SDK functions, parameters, and behaviors should be documented, and creating beautiful, functional documentation is an essential part of making your SDK usable. You can roll your own documentation and add Markdown to the project `README.md` file or use a popular library like [Sphinx](https://www.sphinx-doc.org/) that includes such features as automatic highlighting, themes, and HTML or PDF outputs. ### Linting Consistently formatted code improves readability and encourages contributions that are not jarring in the context of the existing codebase. While this guide doesn't cover linting, linting your SDK code should form part of creating an SDK. For best results, linting should be as far-reaching and opinionated as possible, with little to zero default configuration required. Some popular options for linting include [Flake8](https://pypi.org/project/flake8/), [Ruff](https://github.com/astral-sh/ruff), and [Black](https://pypi.org/project/black/). ### Licensing A project's license can significantly influence the type and number of potential open-source contributors. Choosing the right license for your SDK is key. Do you want your SDK's license to be community-orientated? Simple and permissive? To preserve a particular philosophy on enforcing the sharing of code modifications? In this example, we selected the MIT license for simplicity and ease of use. If you're unsure which license would best suit your needs, consider using a tool like [Choose a license](https://choosealicense.com/). ### Other Endpoints This tutorial covered a basic SDK for two endpoints. Consider that real-world SDKs can have many more endpoints and features. You can use the same principles to add more endpoints to your SDK. ### Authentication Methods If APIs use OAuth 2.0 or other authentication methods, you'll need to add authentication methods to your SDK. You can use libraries like [Authlib](https://docs.authlib.org/en/latest/) to handle OAuth 2.0 authentication. ### Pagination If the API returns paginated results, you'll need to handle pagination in your SDK. ### Retries and Backoff If the API is rate-limited or has intermittent failures, you'll need to add retry and backoff logic to your SDK. ## Build the Package Now that we have a working SDK, we can build the package for distribution. ```bash hatch build ``` Hatch will build the package and output the distribution files: ```bash ────────────────────────────────────── sdist ─────────────────────────────────────── dist/ecom_sdk-0.0.1.tar.gz ────────────────────────────────────── wheel ─────────────────────────────────────── dist/ecom_sdk-0.0.1-py3-none-any.whl ``` You should now have a `./dist` directory containing your source (`.tar.gz`) and built (`.whl`) distributions: ```bash . ├── dist │ ├── ecom_sdk-0.0.1-py3-none-any.whl │ └── ecom_sdk-0.0.1.tar.gz ``` ## Upload the SDK to the Distribution Archives We now have just enough to upload the SDK to the PyPI test repo at test.pypi.org. To upload the build to TestPyPI, you need: 1. A test PyPI account. Go to [https://test.pypi.org/account/register/](https://test.pypi.org/account/register/) to register. 2. A PyPI API token. Create one at [https://test.pypi.org/manage/account/#api-tokens](https://test.pypi.org/manage/account/#api-tokens), setting the "Scope" to "Entire account". Don't close the token page until you have copied and saved the token. For security reasons, the token will only appear once. If you plan to automate SDK publishing in your CI/CD pipeline, you should create per-project tokens. For automated releasing using CI/CD, it is recommended that you create per-project API tokens. Finally, publish the package distribution files by executing: ```bash hatch publish -r test ``` The `-r test` switch specifies that we are using the TestPyPI repository. Hatch will ask for a username and credentials. Use `__token__` as the username (to indicate that we are using a token value rather than a username) and paste your PyPI API token in the credentials field. ```bash hatch publish -r test ⇐ [15:13]═ Enter your username [__token__]: __token__ Enter your credentials: (paste your token here) dist/ecom_sdk-0.0.1-py3-none-any.whl ... success dist/ecom_sdk-0.0.1.tar.gz ... success [ecom-sdk] https://test.pypi.org/project/ecom-sdk/0.0.1/ ``` Your shiny new package is now available to all your new Python customers! The `ecom-sdk` python package can now be installed using: ```bash pip install --index-url https://test.pypi.org/simple/ --no-deps ecom-sdk ``` We use the `--index-url` switch to specify the TestPyPI repo instead of the default live repo. We use the `--no-deps` switch because the test PyPI repo doesn't have all dependencies (because it's a test repo) and the `pip install` command would fail otherwise. ## Automatically Create Language-Idiomatic SDKs From Your API Specification We've taken the first few steps in creating a Python SDK but as you can see, we've barely scratched the surface. It takes a good deal of work and dedication to iterate on an SDK project and get it built right. Then comes the maintenance phase, dealing with pull requests, and triaging issues from contributors. You might want to consider automating the creation of SDKs with a managed service like Speakeasy. You can quickly get up and running with SDKs in nine languages with best practices baked in and maintain them automatically with the latest language and security updates. # how-to-generate-a-mock-server Source: https://speakeasy.com/blog/how-to-generate-a-mock-server import { CodeWithTabs, Table } from "@/mdx/components"; Testing your SDK against your API can be challenging. During development, you often need to spin up a local server as well as a database and other dependencies, which can slow down the process. You're also prone to network issues, rate limits, and other external factors that can disrupt development and testing. Mock servers provide a way to simulate your API's behavior without needing the actual API to be running. They're especially useful when you need to test how your SDK handles specific behaviors – like rate limiting or malformed responses – without relying on unpredictable or hard-to-reproduce real-world conditions. In this guide, we'll walk through how to generate a mock server using Speakeasy and how to test your SDK against it. We'll also discuss the advantages of using a mock server during SDK development and compare Speakeasy with other mock server solutions. ## What is a mock server? A mock server is a virtual API endpoint that simulates the behavior of a real API server. It intercepts test HTTP requests and returns predefined responses according to your OpenAPI document. This means you can test your SDK without connecting to a live API. Think of a mock server as a stand-in actor during a rehearsal. It follows a script (your OpenAPI document) and responds exactly as needed, every time. This consistency is invaluable for ensuring thorough testing across various scenarios, from successful responses to error conditions. It also significantly speeds up the development process by removing the need to wait for real API responses. ```mermaid graph TD A[Unit Test] -->|1 Setup expectations| C[Mock Server] A -->|2 Tests| B[SDK] B -->|3 Sends expected request| C C -->|4 Returns simulated response| B C -->|5 Verify requests using assertion| A ``` ## Why use mock servers? In development, you need to verify how your SDK: - Manages rate limits - Handles network timeouts - Processes malformed responses While Speakeasy-generated SDKs handle these scenarios gracefully, you still need to test them to ensure your SDK behaves as expected. A mock server allows you to simulate these scenarios reliably, without waiting for them to occur in a real environment, ensuring your SDK is thoroughly tested before it reaches production. Additionally, mock servers allow you to **test your SDK in complete isolation**. Instead of depending on external services, you can execute local tests that provide consistent, predictable responses every time. This stability has significant advantages if your backend experiences intermittent downtime or if you want to minimize the impact of outside variables like network latency or downtime windows. When using Speakeasy, the mock server aligns closely with your OpenAPI document, which means that simulated responses closely mirror the behavior of your existing API. While generating your SDK, Speakeasy automatically creates a mock server based on your OpenAPI document, eliminating time-consuming manual coding. It even generates a suite of tests that interact with the server, giving you a strong baseline of coverage for your endpoints right out of the box. As your API evolves, you can regenerate and update both the mock server and the test suite whenever you regenerate your SDK, meaning your simulated mock environment and tests stay in sync with the latest version of your OpenAPI document. ```mermaid sequenceDiagram Tests ->> SDK: Makes calls SDK ->> MockServer: Sends request MockServer ->> SDK: Match request & return mocked response SDK ->> Tests: Verifies results ``` ## Generating a mock server with Speakeasy To generate a mock server using Speakeasy, we'll use our [example FastAPI server](https://github.com/speakeasy-api/examples/tree/main/frameworks-example-fastapi-travel-server), which includes an OpenAPI document. ### 1. Clone the examples repository Begin by cloning the the Speakeasy examples repository. This will serve as our base project. ```bash clone git@github.com:speakeasy-api/examples.git ``` Go to the FastAPI example: ```bash cd examples/frameworks-example-fastapi-travel-server ``` ### 2. Set up Speakeasy If you haven't installed the Speakeasy CLI yet, follow these steps to get started: - **macOS (Homebrew):** ```bash brew tap speakeasy-api/tap brew install speakeasy ``` - **macOS/Linux (Shell Script):** ```bash curl -fsSL https://raw.githubusercontent.com/speakeasy-api/speakeasy/main/install.sh | bash ``` - **Windows (Scoop):** ```powershell scoop bucket add speakeasy-api https://github.com/speakeasy-api/speakeasy scoop install speakeasy ``` You can verify the installation by running the following command: ```bash speakeasy --version ``` Next, authenticate your CLI with the Speakeasy platform: ```bash speakeasy auth login ``` For more detailed installation instructions, refer to the [getting started guide](/docs/speakeasy-reference/cli/getting-started). ### 3. Generate the SDK In the cloned repository, use the Speakeasy CLI to generate an SDK from the OpenAPI document. ```bash speakeasy quickstart -o ./sdk -s openapi.yaml -t python ``` Answer the prompts to configure the SDK generation process. The resulting SDK will be saved in the `/sdk` directory. ### 4. Modify the `gen.yaml` file Navigate to the generated SDK directory and locate the `.speakeasy` folder. ```bash cd sdk/.speakeasy ``` The `gen.yaml` file in the `.speakeasy` folder contains the configuration for the SDK generation process. We'll modify this file to enable mock server generation. Open the `gen.yaml` file in your text editor: ```bash nvim gen.yaml # or your preferred editor ``` Add the `tests` configuration to the `gen.yaml` file and set `generateNewTests` to `true` to enable mock server generation and create test files. ```yaml filename=".speakeasy/gen.yaml" configVersion: 2.0.0 generation: devContainers: enabled: true schemaPath: openapi.yaml sdkClassName: HolidayDestinations maintainOpenAPIOrder: true usageSnippets: optionalPropertyRendering: withExample useClassNamesForArrayFields: true fixes: nameResolutionDec2023: true parameterOrderingFeb2024: true requestResponseComponentNamesFeb2024: true auth: oAuth2ClientCredentialsEnabled: true oAuth2PasswordEnabled: true tests: generateNewTests: true ``` ### 5. Generate the mock server Next, regenerate the SDK with the updated configuration. Run the following command from a terminal in your SDK directory: ```bash speakeasy run ``` A new `/tests` directory will be created in the `./sdk` folder, containing the mock server and test files. ```bash filename="SDK directory structure" sdk ├── .devcontainer ├── .gitattributes ├── .gitignore ├── .speakeasy ├── CONTRIBUTING.md ├── README.md ├── USAGE.md ├── docs ├── poetry.toml ├── py.typed ├── pylintrc ├── pyproject.toml ├── scripts ├── src └── tests ├── __init__.py ├── mockserver │ ├── Dockerfile │ ├── Makefile │ ├── README.md │ ├── go.mod │ ├── go.sum │ ├── internal │ ├── testdata │ └── main.go ├── test_client.py ├── test_destinations.py └── test_general.py ``` ### 5. Run the mock server Navigate to the mock server directory: ```bash cd tests/mockserver ``` Start the mock server: ```bash go run . ``` You now have a fully operational mock server to simulate API responses and test your SDK without relying on the actual API. For more information on running the mock server, read the generated `README.md` file in the `tests/mockserver` directory. ## Testing against the mock server Let's look at how you can unit test your SDK against the mock server. The generated SDK includes test files that you can use to validate the behavior of your SDK. To run the tests, you'll need to install `pytest` and a few other modules listed in the `pyproject.toml` of the generated SDK. Open a new terminal window and install the dependencies. It's best to install in a virtual environment. ```bash pip install pytest httpx pydantic typing-inspect ``` Navigate to the `tests` directory in the SDK and run the tests: ```bash pytest ``` The generated tests will be run against the mock server, validating that your SDK behaves as expected. ## Comparison with other mock server solutions Several other mock server solutions are available, each with its own set of features and limitations. - **[Prism](https://github.com/stoplightio/prism):** An open-source API mocking tool that uses OpenAPI documents to generate mock servers. - **[MockServer](https://www.mock-server.com/):** A Java-based tool for mocking HTTP/HTTPS requests. - **[Postman](https://www.postman.com/):** An API development platform that includes mocking capabilities. The table below compares Speakeasy with these mock server solutions.
## Why choose Speakeasy for test generation and mocking? Speakeasy simplifies two key parts of your SDK development workflow: **test generation** and **mocking**. Here's how: 1. **Immediate test suite** Speakeasy generates a suite of tests when you generate your SDK. Each test aligns with a specific API endpoint, so you can quickly validate both regular and edge-case scenarios without writing boilerplate code. 2. **Automatic mock server** With one command, Speakeasy can start a mock server based on your OpenAPI document. You won't have to set up separate testing environments or craft mock responses by hand, making local testing easier. 3. **Isolated and repeatable testing** Because tests run against a mock server, you aren't affected by network issues or external service downtime. The environment remains consistent, so test results are reliable and easy to reproduce. 4. **Comprehensive endpoint coverage** When Speakeasy generates tests alongside a mock server, it covers every endpoint in your API document. This helps you catch issues early and maintain higher test coverage. 5. **Easy regeneration** As your API changes, update your OpenAPI document. Speakeasy regenerates the mock server and tests, ensuring everything remains in sync with minimal effort on your part. By handling **test generation** and **mocking**, Speakeasy frees you from having to constantly devise tests and maintain testing environments through your development cycle. ## Next steps For a deeper dive into testing, take a look at our [guide to API contract test generation](/post/release-contract-testing), which covers automated testing and validating API contracts. # GOOD Source: https://speakeasy.com/blog/how-to-set-operationid import { Callout } from "@/mdx/components"; Hi! These blog posts have been popular, so we've built an entire [OpenAPI Reference Guide](/openapi/) to answer any question you have. It includes detailed information on [**operations**](/openapi/paths/operations). Happy Spec Writing! ## Intro The OpenAPI spec is best known for descriptions of RESTful APIs, but it's designed to be capable of describing any HTTP API whether that be REST or something more akin to RPC based calls. That leads to the spec having a lot of flexibility baked-in; there's a lot of ways to achieve the exact same result that are equally valid in the eyes of the spec. Because of this, [the OpenAPI docs](https://spec.openapis.org/oas/v3.1.0#operation-object) are very ambiguous when it comes to telling you how you should define your API. That's why we'd like to take the time to eliminate some of the most common ambiguities that you'll confront when you build your OpenAPI spec. In this case we'll be taking a look at **operationId.** ## Common OpenAPI Problem - OperationId A common problem developers face is the handling of operationId (the name of a particular endpoint). The spec lists it as completely optional, but not including it will become a blocker when attempting to use your API Spec with tooling built to generate docs, SDKs, etc from your spec. Now let's take a look at the naming convention for operationId. The [official OpenAPI documentation](https://spec.openapis.org/oas/v3.1.0#operation-object) defines operationId as: “A unique string used to identify the operation. The id MUST be unique among all operations described in the API. The operationId value is case-sensitive. Tools and libraries MAY use the operationId to uniquely identify an operation, therefore, it is RECOMMENDED to follow common programming naming conventions.” While the IDs are case-sensitive, there are a number of tools and programming languages that might be targeted that will require case-insensitivity, case-insensitivity can also help with the readability of your spec. So if you are planning on using your OpenAPI spec to generate downstream artifacts like docs, SDKs, Server stubs, etc, it might be worthwhile ensuring your IDs are named consistently, descriptively and uniquely enough. ## How to write OpenAPI Spec - OperationId Our recommendation would be to **treat the operationId as a required field,** and **make each operationId unique** within the document **treating it as case insensitive**, while avoiding using anything other than alphanumerics and simple separators such as hyphens and underscores. Also be sure to avoid the potential of operationIds being non-unique when any separators are removed (which can happen with a number of tools as they convert your spec to code). Treating operationIds this way gives you a number of advantages: - Avoids name conflicts when generating things such as code for SDKs, server stubs etc - Provides a simple way to refer to your various endpoints in your openapi spec, both for yourself and the consumers of your API. - Provides additional consistency to your OpenAPI spec. Here are some examples of good and bad operationIds: ```yaml ## The below examples are all unique within the document and are avoiding use of anything but simple separators: operationId: "getPetByTag" ... operationId: "add-pet" ... operationId: "findpetsbystatus" ... operationId: "update_pet_with_form" # BAD ## The below examples are of operationIds that while different, are not unique within the document when treated case-insensitively and separators are removed (a common practice for code gen): operationId: "getPetById" ... operationId: "getpetbyid" ... operationId: "get-pet-by-id" ## The below examples generally cause issues for a lot of tooling: operationId: "getPet(ById)" ... operationId: "$getPetById" ... operationId: "" ... operationId: "get pet by id" ... operationId: "getPet byId" ``` # how-we-built-cli Source: https://speakeasy.com/blog/how-we-built-cli Like many startups, the original product we worked on isn't the one that ended up getting product-market fit. One of the side effects of our pivot is that our users were navigating a UX that had been patched in order to support the new core use case (SDK Creation). Not ideal. To address the situation, we spent the last month focused on building a **revamped CLI experience** to establish the foundation for a new standardized SDK creation and management workflow. This is the first step in creating a DX (Developer Experience) that will eventually unify the creation of API artifacts locally, in production, or via the web. In this article we'll walk through our motivation for doing a v2, what principles guided the development process, and how we used [Charm's libraries](https://charm.sh/) to implement a best-in-class CLI. We hope that this can be both an inspiration and guide to other teams considering similar projects.
## Why did we rebuild our CLI? The decision to focus the efforts of our onboarding rebuild on a new CLI was because of the nature of our product. Our product generates libraries, which are output as a set of files. We therefore wanted our DX to be as close to our user's file management workflows as possible. The made focusing on CLI as the natural choice. Our focus on CLI isn't a comment about the importance of traditional GUI-based UX. That too is an important part of DX, and one that we plan on tackling now that we've established a unified workflow. Even if CLI isn't quite as important as UI for your product, building your CLI first may still be a good idea. A CLI should be the core actions a user might take while using your product, with all the veneer stripped away. Building it is a forcing function to make sure you carefully consider and design the base set of commands, which you will later assemble into more complex workflows. It is a natural starting point. As for the timing, the rebuild was triggered by two motivating forces: 1. **Need for new primitives**: As mentioned, we needed a fresh new workflow design that was both optimized for our core use case: artifact creation from an OpenAPI spec, and flexible enough to keep pace with the company's aggressive vision for the future. 2. **New tooling enables new possibilities**: We became aware of [Charm.sh](https://charm.sh/) and their libraries to “make the command line glamorous.” in late 2023. The example apps on their website blew us away and had us playing with ideas we hadn't thought previously possible. With such great tools available to us, there was no excuse for providing users with a mediocre CLI experience. ## What were our principles? For anyone who is building a CLI, we strongly recommend that they first read through the [Command Line Interface Guidelines](https://clig.dev/). Don't take it as gospel, but it's an incredibly useful guide authored by some of the folks who built `docker compose`. The advice may seem at times obvious, but often the best advice is 🙂 - **Assume no prior context** - One of the consistently annoying things about CLIs is that they are often the functional equivalent of a set of magic incantations. Say it correctly, and you're successful. Get it even a bit wrong, and no rabbit comes out of the hat. That means that before you can use a CLI functionally, you need to go and read through the instruction set and match it against your use case. Our goal is for anyone to be able to install our CLI, enter `speakeasy` and just by interacting with it, understand the full scope of what's possible and how they should use it. No docs required. - **Make it Conversational** - Maybe this will change, but at least for the time being, most CLI users are still human. And yet most CLIs don't really cater to people. Most CLIs don't provide any feedback when you're using them and provide no information on common patterns you might want to follow. It's a choose your own adventure. There's no reason it has to be this way. Our goal is for the CLI to provide a guided experience, alongside the rawer primitives. - Include prompts as an alternative to flags for providing information. - Provide suggestions when an input is incorrect. - When an action is complete, we indicate the likely next action. - **Keep it simple** - When presenting users with a CLI's commands, there's often a well-intentioned desire to exhaustively describe everything that's possible. That typically backfires and what's important gets lost. To combat bloat, we adhere to a few rules: - Avoid deep nesting of commands wherever possible. - Use sensible defaults rather than asking the user for information. The sum total of these rules was the creation of a compact and easily grok-able CLI that someone could understand and use without having any previous understanding of Speakeasy. Armed with our design philosophy we set out to build our CLI. ## Why and How Did we Use Charm? It worth stating that we're in no way being sponsored by [Charm](https://charm.sh/) to write this section of our blog post. But if anyone is looking to build a CLI, there is really no substitute for the tooling developed by the Charm team. We're a Go shop, so there was no learning curve, but even if you're not, it's probably worth learning Go just so that you can build on top of their libraries. The best analogy for explaining the value of Charm is that it's like React for Terminal UI. It makes it possible to build beautiful user experiences at a fraction of the cost. To build our new CLI we made use of several of Charm's libraries which we'll walkthrough in more detail. ### Bubble Tea to build our foundation In the React analogy, [Bubble Tea](https://github.com/charmbracelet/bubbletea) is the equivalent of the components framework. We used Bubble Tea as the framework for defining our Terminal application. Some of the more sophisticated aspect of the CLI were built as custom components. For example, our Validation table: ```go filename="cliVisualizer.go" func (m cliVisualizer) Update(msg tea.Msg) (tea.Model, tea.Cmd) { switch msg := msg.(type) { case tea.KeyMsg: if msg.String() == "ctrl+c" || msg.String() == "esc" { m.status = StatusFailed return m, tea.Quit } case UpdateMsg: switch msg { case MsgUpdated: return m, listenForUpdates(m.updates) // wait for next event case MsgSucceeded: m.status = StatusSucceeded return m, tea.Quit case MsgFailed: m.status = StatusFailed return m, tea.Quit } } var cmd tea.Cmd m.spinner, cmd = m.spinner.Update(msg) return m, cmd } ```
Bubble tea makes it possible to easily create delightful experiences that users don't associate with the Terminal. ### Lip Gloss for custom styling [Lip Gloss](https://github.com/charmbracelet/lipgloss) is the equivalent of custom CSS. The custom coloring, text formatting, and selection highlighting in our CLI is all being controlled by our Lip Gloss rules. In particular we make heavy use of Adaptive colors, which ensure that the CLI looks good no matter what our user's terminal theme is. Is it critical to the functioning of the CLI? No. But it's those nice small touches that make the CLI into something that is recognizable as a Speakeasy experience: ```go filename="styles.go" var ( Margins = lipgloss.NewStyle().Margin(1, 2) HeavilyEmphasized = lipgloss. NewStyle(). Foreground(Colors.Yellow). Bold(true) Emphasized = HeavilyEmphasized.Copy().Foreground(Colors.White) Info = Emphasized.Copy().Foreground(Colors.Blue) Warning = Emphasized.Copy().Foreground(Colors.Yellow) Error = Emphasized.Copy().Foreground(Colors.Red) Focused = lipgloss.NewStyle().Foreground(Colors.Yellow) FocusedDimmed = Focused.Copy().Foreground(Colors.DimYellow) Dimmed = lipgloss.NewStyle().Foreground(Colors.Grey) DimmedItalic = Dimmed.Copy().Italic(true) Help = DimmedItalic.Copy() Success = Emphasized.Copy().Foreground(Colors.Green) Cursor = FocusedDimmed.Copy() None = lipgloss.NewStyle() ``` ### Using Huh to build Quickstart [Huh](https://github.com/charmbracelet/huh) is Charm's latest & greatest library (released December 2023). It is a form builder for terminals. Despite being so new, we jumped on the opportunity to use it. It was a perfect fit foundation for our `speakeasy quickstart` experience. If you agree that CLI experiences should resemble a conversation, then providing users with prompts to keep them moving through the workflow is critically important. We've built our quickstart command to wrap our other command primitives (`configure` & `run`) to give users a fully guided onboarding experience, ```go filename="quickstart.go" func PromptForNewTarget(currentWorkflow *workflow.Workflow, targetName, targetType, outDir string) (string, *workflow.Target, error) { sourceName := getSourcesFromWorkflow(currentWorkflow)[0] prompts := getBaseTargetPrompts(currentWorkflow, &sourceName, &targetName, &targetType, &outDir, true) if _, err := tea.NewProgram(charm.NewForm(huh.NewForm(prompts), "Let's setup a new target for your workflow.", "A target is a set of workflow instructions and a gen.yaml config that defines what you would like to generate.")). Run(); err != nil { return "", nil, err } ... } ```
### What's Next Rebooting our CLI was the first step of a complete overhaul to our onboarding workflow. We're now working on two related projects to complete the end to end workflow. 1) Moving our [Github action](https://github.com/speakeasy-api/sdk-generation-action) to use the new CLI primitives, for a single source of truth. 2) Creating a sync with Github so that users can easily iterate on their production SDKs. Have ideas on what we should build next? Have thoughts on the new CLI experience? We'd love to hear them, please feel free to [join our public slack](https://go.speakeasy.com/slack) and let us know what you think. # how-we-built-universal-ts Source: https://speakeasy.com/blog/how-we-built-universal-ts import { Callout } from "@/mdx/components"; Try out our new TypeScript generation for yourself. Download our CLI to get started: ```bash brew install speakeasy-api/tap/speakeasy ``` In this blog post, we'll share the story behind why we decided to build v2 of our typescript generation. And talk through some of the critical design choices we made along the way. Whether you're a seasoned TypeScript developer or just starting out, we hope you'll find something valuable in this article. ## Learning from V1 Let's start by talking about the [open-source generators](https://openapi-generator.tech/docs/generators/); there are a lot of them (11 for TypeScript currently). It'd be fair to ask, why do we need another one, and why would someone pay for it. None of the existing generators are good enough to build SDKs for an enterprise API offering (without a fair amount of custom work). We're very glad that the open-source generators exist. They're great for experimentation and hobby projects, but they fall short of serving enterprises (which, to be fair, was never the intent of the OSS project). We rolled out the first version of our TypeScript generator exactly 1 year ago. Our goal was to build an SDK generator that could serve enterprise use cases. Our initial requirements were: - Support for OpenAPI 3.0 & 3.1 - Idiomatic TypeScript code - Runtime Type safety for both request and response payloads - Support for Node LTS - Support for retries & pagination - CI/CD Automation for building & publishing We satisfied all of these requirements, but weren't ourselves satisfied with the result. We hadn't built something that was really great. It was certainly better than the open-source offerings available, but we found ourselves frustrated with a few aspects of the generator: 1. Our approach to de/serialization had us in a straight jacket. We had decided to use classes with decorators. This approach had several limitations and made it difficult to maintain and extend our codebase. Specifically, it impeded our ability to build support for Union types. We needed a more flexible solution that would allow us to better support the evolving needs of our users without sacrificing runtime type safety. ```typescript filename="OldPet.ts" export class Pet extends SpeakeasyBase { @SpeakeasyMetadata({ data: "form, name=category;json=true" }) @Expose({ name: "category" }) @Type(() => Category) category?: Category; @SpeakeasyMetadata({ data: "form, name=id" }) @Expose({ name: "id" }) id?: number; @SpeakeasyMetadata({ data: "form, name=name" }) @Expose({ name: "name" }) name: string; @SpeakeasyMetadata({ data: "form, name=photoUrls" }) @Expose({ name: "photoUrls" }) photoUrls: string[]; /** * pet status in the store */ @SpeakeasyMetadata({ data: "form, name=status" }) @Expose({ name: "status" }) status?: PetStatus; @SpeakeasyMetadata({ data: "form, name=tags;json=true", elemType: Tag }) @Expose({ name: "tags" }) @Type(() => Tag) tags?: Tag[]; } ``` 2. Our SDK generation was overly focused on server-side usage. Understandable, because this was the majority of our usage. But more and more JavaScript applications are being built where the line between server responsibility and client responsibility are blurred. In these applications, both rely on a common set of libraries. We wanted to cover this use case. 3. Finally, the rise of AI APIs in the last year had created new and growing demand for TypeScript SDKs that could better handle long running data streams. So three months ago, we embarked on the journey of building a new TypeScript generator to overcome these deficiencies. ## How we designed our new generator A theme that we're going to keep coming back to is, "Use the platform". We're building a TypeScript generator, so we use TypeScript primitives wherever possible. We're building a generator for OpenAPI, so we use OpenAPI primitives wherever possible. Seems straightforward, but sometimes it's easy to get caught up in the excitement of building something new and forget to take advantage of the tools that are already available to us. ### Rebuilding Type Safety with Zod Validation is really important when you talk about APIs. By making your API's inputs explicit, developers can debug in their IDE as they write the application code, sparing them the frustration of having to compare constructed data object to API docs to see where mistakes occurred. This is even more important in the world of JavaScript, where users will pass you anything, and your types don't exist at runtime. As we mentioned, our first attempt worked at plugging the hole of runtime type safety, but it had downsides. Adding support for more complex types was overly difficult. In our second attempt, we turned to Zod: "A TypeScript-first schema validation library that allows you to define schemas from a simple `string` to a complex nested object." Our TypeScript generator creates Zod schemas for all the request and response objects in a users OpenAPI spec: ```typescript filename="product.ts" export namespace ProductInput$ { export type Inbound = { name: string; price: number; }; export const inboundSchema: z.ZodType = z .object({ name: z.string(), price: z.number().int(), }) .transform((v) => { return { name: v.name, price: v.price, }; }); export type Outbound = { name: string; price: number; }; export const outboundSchema: z.ZodType = z .object({ name: z.string(), price: z.number().int(), }) .transform((v) => { return { name: v.name, price: v.price, }; }); } ``` We then use these schemas to validate the request and response payloads at runtime: ```typescript filename="zodExample.ts" import { SDK } from "@speakeasy/super-sdk"; async function run() { const sdk = new SDK(); const result = await sdk.products.create({ name: "Fancy pants", price: "ummm", }); } run(); // 🚨 Throws // // ZodError: [ // { // "code": "invalid_type", // "expected": "number", // "received": "string", // "path": [ // "price" // ], // "message": "Expected number, received string" // } // ] ``` Our validation also goes beyond user input, by validating the server's responses. We will guarantee that what your API is providing to users is correct, as per your spec. And if it's not, we fail loudly. There's no hidden errors for users to parse through. #### Validation Tradeoffs It's worth acknowledging that there are tradeoffs to using Zod. The biggest one is having a 3rd party dependency in the library. We've tried hard to keep our generated libraries free of them because of the security risks they pose. However, validation is a critical feature, and Zod doesn't pull in any additional dependencies, so we felt it was well worth it. Additionally, we've encountered a couple of truly enormous OpenAPI specs that have resulted in huge SDKs. These can suffer some performance regressions from having to type-check all the way through. It's an edge case, and one that we're working on some heuristics to mitigate, but it's worth noting. ### Going TypeScript Native for Client Support Most SDKs you encounter will live in repos like `acme-sdk-node` or `acme-sdk-front-end`, but these qualifiers don't need to exist anymore. The primitives needed to run a feature-rich Typescript SDK across all the major runtimes are available. And that what we set out to build The biggest changes that we needed to make were dropping our dependency on Axios and moving to the native `fetch` API. We now have a single codebase that can be used in any environment, including Node.js, browsers, and serverless functions. Other changes we needed to make to better support client-side usage were: - Enabling tree-shaking - we decoupled the SDKs' modules wherever possible. If SDKs are subdivided into namespaces, such as `sdk.comments.create(...)`, it's now possible to import the exact namespaces, or "sub-SDKs" as we call them, and tree-shake the rest of the SDK away at build time. #### Universality Tradeoffs If you set out to build an SDK per runtime, like an SDK for node, an SDK for bun, you will be able to leverage more native APIs that in some instances could perform better than the primitive APIs we use. For example, Node's [stream library](https://nodejs.org/api/stream.html) is a little bit more performant than the web streams one, but we think the trade off that we made is valuable in the long run and better at enterprise scale. It's cognitively simpler to distribute and talk about one SDK. It allows you to manage one set of documentation, and maintain one education path for users. That's invaluable to an org operating at scale. ### Adding Support for Data Streams The last major feature we wanted to add was support for data streams. When we talk about data streams, we're talking about support for two different things, both of which are important for AI APIs. The first is traditional file uploads and downloads. Previously, we had supported this where the entire file was loaded into memory before being sent to the server. This is fine for small files, but isn't workable for large ones. We shifted our file support to use the `Blob()` and `File()` APIs, so that files could be sent to/from APIs via a data stream. We base that on whether your OpenAPI spec marks a certain field as binary data. If it does we'll automatically generate a type for that field such that your users can pass a blob-like object or an entire byte array to upload. On the response side, if we know that the response is binary data, then users will be given a readable stream to consume. Finally, there's another type of streaming that we've built support for: server-sent events (SSEs). SSEs have gained popularity recently as a way to stream data from the server to the client. They're a great fit for AI APIs, where you might be streaming the results of a long running job. We've built a straightforward implementation that doesn't require any proprietary extensions in your OpenAPI spec. Just mark the response as a `text/event-stream` and we'll automatically generate a type for it. We'll also generate a `stream` option on the request object that will return an async iterable that you can use to consume the stream: ```typescript filename="sse.ts" import { SDK } from "@speakeasy/super-sdk"; async function run() { const sdk = new SDK(); const result = await sdk.chat({ stream: true, messages: [ { role: "user", text: "Tell me three interesting facts about Norwegian Forest cats.", }, ], }); if (!result.chatStream) { throw new Error("expected completion stream"); } for await (const event of result.chatStream) { process.stdout.write(event.data.content); } // 👆 gradually prints the chat response to the terminal } run(); ``` ## Summary So that's it. We're really proud of the new TypeScript generator that we've built. We think it's better than any other TypeScript generation that's out there, and is on par with even the best hand-written SDKs. We're interested in hearing people's feedback, so please give it a go yourself. Join [our public slack](https://go.speakeasy.com/slack) and let us know what you think! # how-we-reduced-token-usage-by-100x-dynamic-toolsets-v2 Source: https://speakeasy.com/blog/how-we-reduced-token-usage-by-100x-dynamic-toolsets-v2 import { Callout } from "@/mdx/components"; import { CalloutCta } from "@/components/callout-cta"; import { GithubIcon } from "@/assets/svg/social/github"; } title="Gram OSS Repository" description="Check out Github to see how it works under the hood, contribute improvements, or adapt it for your own use cases. Give us a star!" buttonText="View on GitHub" buttonHref="https://github.com/speakeasy-api/gram" /> If you've been following the discussion around MCP (Model Context Protocol) lately, you've probably noticed the grumbling on Hacker News and elsewhere. Critics love to point out that MCP is a "context hog," burning through tokens at an alarming rate when you try to expose more than a handful of tools to an AI agent. This criticism reveals a fundamental tension in AI tooling: tools are incredibly useful for making LLMs productive, but letting the LLM know about those tools is context-expensive. Load up hundreds of API operations and you'll quickly hit context window limits or face prohibitive token costs. Lots of people like to complain about the challenges of making LLMs useful and then blame it on MCP. At Speakeasy, we prefer to use the protocol to solve problems. Here are the results. We've refined our Dynamic Toolset implementation for MCP servers, creating an approach that delivers the best of both worlds: the discoverability of progressive search and the efficiency of semantic search. Our latest benchmarks show up to 160x token reduction compared to static toolsets while maintaining 100% success rates across diverse tasks. The new Dynamic Toolset approach reduces token usage by an average of 96% for inputs and 90% for total token consumption, making it practical to build MCP servers with hundreds of tools without overwhelming the LLM's context window. Dynamic Toolsets are available in Gram today for all MCP servers! ## Learning from our previous approach Earlier this month, we [introduced Dynamic Toolsets](/blog/100x-token-reduction-dynamic-toolsets) with two experimental approaches: progressive search and semantic search. Both delivered significant token reductions, but our production experience revealed important insights: **Semantic search was highly effective** at helping LLMs find the right tools within large toolsets using embeddings-based discovery. However, it had a critical limitation: LLMs sometimes wouldn't even attempt to search for tools because they had no visibility into what was available. A vague user prompt like "how many deals do we have?" would cause confusion because the LLM had no idea that a "deal" meant a "HubSpot deal" in this context. **Progressive search provided excellent discoverability** by including an outline of available tool "categories" in the tool description. This helped LLMs understand what tools were available in the first place, increasing the likelihood that they would attempt to search for tools. However, this categorization depends on the quality of the input Gram Functions or OpenAPI documents. Semantic search doesn't have this limitation. We also discovered that adding a separate `describe_tools` function significantly reduced token usage by allowing LLMs to retrieve input schemas only for tools they intended to use, since input schemas represent a large percentage of total tokens. ## Introducing the refined Dynamic Toolset Our new approach combines the strengths of both methods into a unified system that exposes three core tools: **`search_tools`**: Uses embeddings-based semantic search like our previous approach, but now includes an overview of available tool categories in the description. The LLM can also apply filters to narrow searches by tags like `source:hubspot` or `hubspot/deals`. This gives the LLM both discoverability and precise search capability. **`describe_tools`**: Provides detailed schemas and documentation for specific tools. By separating this from search, we optimize token usage since very large input schemas are only loaded when actually needed. **`execute_tool`**: Executes the discovered and described tools. This remains unchanged but benefits from the improved discovery mechanisms. The key insight is that tool descriptions provide structure and discoverability, while semantic search provides efficiency and natural language discovery. Together, they create a system that scales naturally without sacrificing usability. ## Performance results We conducted extensive benchmarking across toolsets of varying sizes (40 to 400 tools) using both simple single-tool tasks and complex multi-tool workflows. ### Token usage comparison ![Token usage comparison](/assets/blog/dynamic-toolsets/results.png) We saw 100% success rates for all toolset sizes and task complexities, where success is defined as the LLM calling the expected (underlying) tools with the expected arguments. Note that we used Claude Sonnet 4.5 for all tests. ### Advantages The data reveals several significant advantages of Dynamic Toolsets: **Massive token reduction**: Input tokens are reduced by an average of 96.7% for simple tasks and 91.2% for complex tasks. Even accounting for increased output tokens from additional tool calls, total token usage drops by 96.4% and 90.7% respectively. **Consistent scaling**: Unlike static toolsets where token usage grows linearly with toolset size, Dynamic Toolsets maintain relatively constant token consumption regardless of whether you have 40 or 400 tools. **Perfect success rates**: The refined approach maintains 100% success rates across all toolset sizes and task complexities, proving that token efficiency doesn't come at the cost of reliability. **Predictable costs**: While tool calls increase by 2x on average, the dramatic reduction in token usage results in overall cost savings, especially for large toolsets where static approaches become prohibitively expensive or impossible due to context window limits. ## Disadvantages **More tool calls**: Dynamic Toolsets require 2-3x more tool calls than a static toolset. This is because the LLM needs to search for the tools it needs to use, and then describe the tools it needs to use. The number of tools calls could be reduced (at the expense of token usage) by combining the `search_tools` and `describe_tools` calls into a single call. **Slower**: More tools calls means more LLM cycles, which means more time to complete the task. We saw an average of ~50% increased execution time for dynamic toolsets compared to static toolsets. We expect that for longer-running agents, the execution time will be closer to (perhaps even less than) the static toolset execution time. This is because as tools are discovered (brought into context), the LLM can reuse them without needing new search/describe calls. ### Why three tool calls instead of one? While it might seem counterintuitive to increase tool calls, the three-step approach is essential for optimal token efficiency: 1. **`search_tools`**: The LLM first searches for relevant tools using natural language queries 2. **`describe_tools`**: The LLM then requests detailed schemas only for the tools it intends to use 3. **`execute_tool`**: Finally, the LLM executes the tools with the proper parameters This separation prevents loading unnecessary schemas upfront while ensuring the LLM has complete information about the tools it chooses to use. Despite increasing tool calls by 2x, this doesn't increase total tokens or runtime because input tokens are reduced by 96%+. ### Tool call patterns For the "complex" workflow shown in our benchmarks, we typically see 6-8 total tool calls instead of the 3 tool calls required for a static toolset: **Typical 7-call pattern** (most common): - 3 separate `search_tools` calls (one for each needed tool) - 1 `describe_tools` call (requesting schemas for all three tools at once) - 3 `execute_tool` calls (one for each tool) **Efficient 6-call pattern** (when search is optimal): - 2 `search_tools` calls (finding multiple tools in combined searches) - 1 `describe_tools` call - 3 `execute_tool` calls **Extended 8-call pattern** (when search needs refinement): - 4 `search_tools` calls (one search doesn't return expected results, particularly common with larger toolsets) - 1 `describe_tools` call - 3 `execute_tool` calls **Enhanced search capability**: The `search_tools` function combines semantic search with structured filtering. Tool descriptions include categorical overviews (e.g., "This toolset includes HubSpot CRM operations, deal management, contact synchronization...") while maintaining the precision of embeddings-based search. **Lazy schema loading**: Input schemas are only loaded via `describe_tools` when the LLM explicitly requests them. Since schemas often represent 60-80% of token usage in static toolsets, this separation delivers significant efficiency gains. **Intelligent reuse**: The conversation history serves as a "cache" for tool descriptions and schemas, reducing redundant token usage in multi-step workflows. ## Why this approach works The success of the refined Dynamic Toolsets comes from addressing the core tension between discoverability and efficiency: **Context awareness**: By including categorical overviews in tool descriptions, LLMs understand what's possible within a toolset before starting their search. **Natural language discovery**: Semantic search allows LLMs to describe their intent naturally ("find tools for managing customer deals") rather than guessing exact function names. **Just-in-time loading**: Only the tools and schemas actually needed for a task consume tokens, keeping context windows focused and efficient. **Scalable architecture**: Adding new tools to a toolset doesn't increase initial token usage, making the approach naturally scalable for growing APIs. ## Getting started Dynamic Toolsets are available in Gram for all MCP servers. To enable them: 1. Navigate to the MCP section in your Gram dashboard 2. Select your toolset and switch from "Static" to "Dynamic" mode 3. Configure any tag-based filters if your toolset includes categorized tools The system handles the transition automatically, maintaining full compatibility with existing MCP implementations while delivering immediate token efficiency benefits. ## The bigger picture MCP gets a lot of criticism, but here's the thing: it's just an implementation detail. At its core, MCP is a flexible protocol that gets out of the way to let you solve real problems. The complaints you hear aren't really about the protocol—they're about the fundamental challenge of building useful AI agents at scale. You could build specialized "code mode" environments that sidestep context limitations entirely, and some teams do. But those solutions require significant engineering investment and domain expertise that most teams simply don't have. The beauty of MCP is that it provides a standardized way to make any API accessible to AI agents, and approaches like Dynamic Toolsets show how the protocol can adapt to solve its own scaling challenges. Dynamic Toolsets represent more than just a token optimization—they demonstrate that MCP can evolve to meet the practical needs of production AI systems. By treating the context window as a resource to be managed rather than a hard constraint to work around, we've opened up new possibilities for building comprehensive AI agents that can work with complex enterprise systems. The future of AI tooling isn't about building perfect protocols from day one. It's about building flexible systems that can adapt as we learn more about what works in practice. MCP provides that flexibility, and we're just getting started. ## Additional reading - [Previous Dynamic Toolsets implementation](/blog/100x-token-reduction-dynamic-toolsets) - [Dynamic Toolsets documentation](/docs/gram/build-mcp/dynamic-toolsets) - [Code execution with MCP](https://www.anthropic.com/engineering/code-execution-with-mcp) - [Tool design best practices](/mcp/tool-design) # improved-devex-with-flattened-sdks Source: https://speakeasy.com/blog/improved-devex-with-flattened-sdks Despite what some NBA players may believe, the world is not flat. But Speakeasy-created SDKs now are! As the user of an API, you don't want to have to understand the technical detail of the API you are using. You just want to understand what input is expected, and what output I should expect in return. Everything else is a distraction. We're raising the bar for API developer experience, and ensuring the SDKs produced by Speakeasy are as easy to use as possible. That's why we're excited to release a new flattened structure to our SDKs. ## New Features **Flattened SDKs:** Users no longer need to understand whether the inputs to your API are query params, path params, headers, or request bodies. The SDK abstracts away all those details. Users just need to provide the relevant data as key-value pairs -- the SDK will construct the correct call to the API. That means less code that your users need to write, more easily understood interfaces, faster integration times, and an overall improved developer experience. You don't need to do anything to generate with the new SDK structure. If you've set up an automated pipeline, it will run with flattened requests as the new default. If you're using the CLI, then simply upgrade to the latest version. Here's an example of before and after the new flattened structure, using the Speakeasy API (SDK) for adding a user to a workspace. **Old:** ```python import speakeasy from speakeasy.models import operations, shared s = speakeasy.SDK() s.config_security( security=shared.Security( api_key_authentication=shared.SchemeAPIKeyAuthentication( api_key="YOUR_API_KEY_HERE", ) ) ) req = operations.AddUserToWorkspaceRequest( path_params=operations.AddUserToWorkspacePathParams( project_id=548814, ), request=shared.AddUserToWorkspaceRequest( user="nolan", ) ) res = s.workspace.add_user_to_workspace(req) if res.generic_api_response is not None: # handle response ``` **New**: ```python import speakeasy from speakeasy.models import operations, shared s = speakeasy.SDK(api_key="YOUR_API_KEY_HERE") req = operations.AddUserToWorkspaceRequest( project_id=548814, add_user_to_workspace_request=shared.AddUserToWorkspaceRequest( user="nolan", ), ) res = s.workspace.add_user_to_workspace(req) if res.generic_api_response is not None: # handle response ``` # integrate-with-your-favorite-docs-provider Source: https://speakeasy.com/blog/integrate-with-your-favorite-docs-provider import { Callout, ReactPlayer } from "@/lib/mdx/components"; As we roll into summer, the days are getting longer but your APIs' time to `200` will be getting shorter thanks to our new integration with API documentation providers! More on this exciting new feature below, as well as the regular deluge of updates to our code generator! Let's get into it 👇 ## SDK Docs Integration Production integration involves a lot more than just making an HTTP request. So why does your API reference have a generic `fetch` call? Authentication, error handling, pagination parsing, request retries, all need to be considered. Thankfully, SDKs abstract a lot of these concerns for users, making it possible to provide a code snippet that is both concise and useful to your users. And with our new docs integration, it's now possible to automatically update your SDK code snippets with every new version of your SDKs! Building a production-ready integration is as easy as ⌘C, ⌘V.
Head to [**our docs**](/docs/mintlify) to start integrating SDKs into **your docs**! ## 🚢 Improvements and Bug Fixes 🐛 Based on the most recent CLI version: [**Speakeasy v1.253.3**](https://github.com/speakeasy-api/speakeasy/releases/tag/v1.231.0) ### The Platform 🚢 Speed up validation runs by skipping name resolution \ 🚢 Global parameters can now be hidden from method signatures by configuring `maxMethodParams` with extensions \ 🚢 Support for flattened globals \ 🚢 `deprecated` OpenAPI keyword now respected for globals ### Terraform 🚢 Always instantiate arrays to empty slices to avoid `null` being sent over the wire \ 🚢 Capability to generate resource state upgraders \ 🚢 Custom set validators, bump `terraform-plugin-framework` to 1.7.0 with custom dependencies \ 🚢 Set types in Terraform, `x-speakeasy-xor-with` / `x-speakeasy-required-with` validators \ 🐛 Fix: extensions being ignored when under an `allOf` \ 🐛 Fix Terraform support in `quickstart` command \ 🐛 Fix: only hoist union subtypes that are primitive to avoid a provider error ### C# 🚢 Support multi-level package naming and non-camel case namespaces ### Python 🚢 Bump pylint version to `3.1.0` in Python \ 🚢 Better `additionalDeps` validation in your `gen.yaml` file # internal-sdks Source: https://speakeasy.com/blog/internal-sdks {/* import { Testimonial } from "~/components"; */} {/* TODO: Add testimonials */} People often think about SDKs in the context of libraries that make it easier for **external** users to integrate with an API. But SDKs are just as as valuable to internal developers working with internal APIs. In fact, internal APIs are where SDKs can provide the most business value: - **Most APIs are internal**: In its [2023 state of the API](https://www.postman.com/state-of-api/a-day-week-or-year-in-the-life/#a-day-week-or-year-in-the-life) report, Postman found that 61% of APIs are for private use only. - **Internal APIs are heavily used**: An external API probably has a single point of integration whereas an internal API is likely reused across a suite of applications in their frontend, backend, and middleware. Because of this, building SDKs for internal APIs can be transformational: - **Increased velocity** - SDKs can reduce the time to integrate with an API by 50% or more. That means more time for innovation and less time on boilerplate. - **Improved API Governance** - SDKs can be a powerful tool for API governance, helping teams work autonomously while maintaining a consistent interface across the organization. - **Better staff retention** - Faster velocity, and less reptition leads to happier and more productive developers, which leads to better staff retention. ## Developer velocity drives business growth For orgs with an API-first approach, ease of integration with internal APIs is critical for business success. SDKs enable this through: - **Eliminates integration boilerplate** - Eliminates the tedious, repititve integration work that needs to be done before integrating with a service. - **Reduces errors** - SDKs mean there's less opprotunity to shoot yourself in the foot. The SDK will handle areas where people will frequently make mistakes, like authentication, error handling, and serialization. - **Speeds up debugging** - SDK wll shrink the surface area of the integration, so when issues do occur, there's less code to dig through. ![simple, non-scientific diagram showing how internal SDKs create an inflection point in dev-velocity and growth](/assets/blog/internal-sdks/diagram-business-velocity-after-speakeasy.png) With SDKs handling so much of the integration work, developers can focus on innovating where it matters, the business logic of their application. And the more teams using your internal SDKs, the faster the gains in velocity compound. ## Greater independence with consistent interfaces Organizations fail when they create lengthy review processes to enforce consistency across development teams. They inevitably kill innovation without solving the consistency challenge. The better strategy is to hand developers tools that speed up their work, and have the side effect of ensuring consistency. Derived from a common API contract, SDKs can be a usual tool to make sure that API consumption practices are consistent acorss the organization. This is a major compenent of a successufl API governance program. Kin Lane [defines API governance](https://apievangelist.com/2021/11/13/some-thoughts-on-api-governance) as: {/* “Ensuring that the complex systems powering the enterprise are as defined, discoverable, and observable as possible so that you can understand the state of the system, and then incrementally evolve the state of the system as a whole, in the right direction.” */} oAs a codebase grows, governance becomes increasingly important. Good SDKs make it easier for teams to make good design choices and harder to make bad ones. ## Happy Developers, Happy Business [A study by McKinsey](https://www.mckinsey.com/industries/technology-media-and-telecommunications/our-insights/developer-velocity-how-software-excellence-fuels-business-performance) found that high developer velocity correlates strongly with many indicators of business health, including talent retention. The study concluded that a major driver of velocity comes from the quality of internal tooling: {/* “The ability to access relevant tools for each stage of the software life cycle contributes to developer satisfaction and retention rates that are 47 percent higher for top-quartile companies compared with bottom-quartile performers.” */} The previous sections have explained how SDKs can bring a level of reusability and consistency to API usage. To understand the impact of that reusability on developers' quality of life, consider a common pattern that manifests at organizations where it's missing: 1. Every team writes bespoke code. No one shares resources. When someone tries to share, they soon find that no one else uses or even recognizes their attempt. 2. The first time two teams discover they have independently done the same thing, it's amusing. Then, this becomes a running joke. But beneath the humor is mounting frustration. 3. Inevitably, people start to feel that their work is unimportant, meaningless. Data inconsistencies pile on top of fragile designs; the codebase begins to feel brittle and perilous; workers feel less and less incentive to innovate; dreams of standardization becomes more and more remote as disarray spreads. 4. All productive engineers are looking for a new job. An SDK cannot save an organization from chaos, but it can heavily contribute to consistency and well-being. ## SDKs are a win-win - Effective organizations encourage teams to work autonomously and discover their own solutions. - Effective organizations benefit from standardized codebases and design patterns. A good SDK program can ease the tension between these apparently contradictory statements. The standardized interface and pre-built libraries help teams work faster, work autonomously, and develop quickly without "going rogue". This harmony promotes both bottom-up creativity and top-down governance. ## OK, but making an SDK is hard work One fact may undermine my whole argument: good SDKs are hard to build and maintain. This is true even with a stable API and a single development language. For fast-moving, polyglottal teams, the maintenance complexity explodes. What if there were a tool to generate quality SDKs automatically, in multiple languages, using taut, idiomatic code, and avoiding heaps of boilerplate? Grab an OpenAPI spec and [try Speakeasy yourself](https://app.speakeasy.com/). # Speakeasy's $11M Raise Source: https://speakeasy.com/blog/introducing-speakeasy import { Callout } from "@/mdx/components"; 4000 SDKs, 100's of Github repos, 9 team members, and 8 languages later... Today, we're excited to [officially announce Speakeasy to the world](https://techcrunch.com/2023/06/29/speakeasy-is-using-ai-to-automate-api-creation-and-distribution/), with $11 million in combined pre-seed & seed funding from backers including: [Crystal Huang](https://twitter.com/CrystalHuang) at [Google Ventures](https://www.gv.com/), [Astasia Myers](https://twitter.com/AstasiaMyers) at [Quiet Capital](https://quiet.com/), [Flex Capital](https://www.flexcapital.com/), [StoryHouse Ventures](https://www.storyhousevc.com/), [Firestreak Ventures](https://www.firestreak.com/), and angels including Amit Agarwal (President of Datadog), Clint Sharp (co-founder and CEO of Cribl) and Todd Berman (CTO of Attentive). To show off all the exciting features we've built this year, we'll be hosting a Feature Week, July 10th-14th. Every day, we'll be posting short deep-dives on what we're busy building here at Speakeasy. [Sign up for our newsletter](/post#subscribe) and we'll send you a reminder! ## Why are we building Speakeasy? There is one thing we can all agree on: APIs are ubiquitous and critical to building modern software. In a single line of code, you are able to harness the collective efforts of a thousand developers. It's not much of an exaggeration to say that APIs give engineering teams superpowers. Yet shifting access patterns and heightened user expectations mean it has never been harder to build a "great" API. Providing the tools necessary for users to easily integrate requires navigating a world of API marketplaces, plugin ecosystems, chat interfaces, a dozen language communities, and more. We're a long way from the days of the humble `curl`. Of course, everyone wants their API to have a great developer experience and be natively accessible in any environment & runtime, but very few are able to commit the huge platform engineering investments required to solve this "final mile" problem. Those who do - such as Stripe, Plaid, Twilio, Github — reap fantastic rewards. Most API teams however are left in the lurch. Between managing new versions, duct taping schemas, and keeping documentation from going stale, their hands are already full. That often places the “final mile” of integration on the shoulders of the user. Their only tools: an API reference page, their own determination, and a high threshold for frustration. As a result, APIs — whether for internal or external users — end up under-used and under-resourced. Neither builder nor user is happy. And yet what's the alternative? Building all the tooling to offer that ideal API DevEx in-house would leave API teams with tons of tech debt that isn't related to their core product. That's why Speakeasy has showed up, with a product, a pipeline, and a team of people to solve this problem once and for all. **📣 Speakeasy's mission is to make it easy to create and consume any API.** ## What We Do Speakeasy provides production-quality developer surfaces for your API that delight users and make integration easy. Today, that means managed native-language SDKs and Terraform providers. Speakeasy SDKs enable your API users, whether external customers or internal teams, to integrate rapidly. We provide a far more intuitive and developer-friendly (even, dare we say, enjoyable?) experience than is possible with the status quo “API reference + sheer determination” approach today. Speakeasy managed SDKs will equip your API users with compilable usage snippets for every function, context-aware in-IDE code completion, and type safety. Users are spared from writing error-prone boilerplate code, dramatically reducing frustration and the number of support tickets. Ultimately, API builders save huge amounts of time and cost, while user adoption is maximized thanks to a world-class API developer experience. Here's what a couple of our customers have to say: > “The engineering team is consistently focused on developing Codat's core infrastructure, and we're always figuring out the most efficient way to advance our developer experience. Finding Speakeasy has been transformational in terms of our team's velocity. We've been able to progress our roadmap faster than we thought possible. We already have SDKs for 3 languages in production. If we'd been on our own, we'd probably be getting ready to publish the first one now.” [David Coplowe, Codat DevEx Team](/post/case-study-codat) > “Speakeasy's unique offering of high-quality SDKs including a Terraform provider, all generated from our existing API specs allowed us to take a huge leap in our own product go-to-market. We don't need to invest in hiring teams of specialist engineers — allowing us to focus on our core product.” [Viljami Kuosmanen, Head of Engineering, epilot](/post/case-study-epilot) ## How does it work? Today, given an API, we: - Automatically maintain your API specs through AI-powered suggestions and telemetry-based drift detection - Create idiomatic, type safe, production-quality SDKs in 7 languages and counting: Python, Java, Typescript, Go, Ruby, PHP, C# — and even new API ecosystems like Terraform Providers. We create and maintain these in polished Github repos. - Customise and brand your SDKs with a batteries-included experience like retries, pagination and auth. You can steer our code generation engine with extensions and simple config. - Generate docs for your SDK. Making it incredibly simple for API consumers to pick up and use in minutes. 1 copy paste to a working integration - Publish SDKs to package managers (npm, PyPI, etc.). No more signign into your sonatype account. - Watch for updates to your API spec, and re-run the entire workflow automatically so your SDKs are always up-to-date - Manage the rollout of your SDKs, client authentication with gateways and self service telemetry in just a few clicks ![Speakeasy manages the entire workflow of SDK and Terraform provider creation: from spec validation/enrichment, through code creation, and package publishing](/assets/blog/introducing-speakeasy/speakeasy-diagram.png) Speakeasy manages the entire workflow of SDK and Terraform provider creation: from spec validation/enrichment, through code creation, and package publishing ## Why We're Different Prior to Speakeasy, the most prevalent options for creating SDKs were: 1. Creating SDKs manually 2. Using open-source generators (yes, the one with >3K issues) Neither of these are great options for most companies. Manually creating SDKs requires costly eng teams to be hired and maintained, and dilutes focus from other core development priorities. Open-source generators are great for hobbyists — but lack the comprehensive commercial support, idiomatic code output, and broad OpenAPI compatibility needed for enterprises. If using the OSS generators, teams ultimately still need to invest a significant amount of eng time in order to support production use cases. **Speakeasy offers a compelling alternative** - SDKs that are fully-managed and supported by our responsive team, providing you with all the benefits of an [API platform](/post/why-an-api-platform-is-important/) team at a fraction of the cost. - A comprehensive pipeline for generating SDKs, Terraform providers, and other developer surfaces: CLIs, Zapier plugins, natural language agents and more. - Integrates seamlessly with your API development. We integrate directly with GitHub, GitLab, and other CI/CD platforms ensuring SDKs are updated on every release. - Code generation built from the ground up, with a focus on creating customisable, idiomatic, fault tolerant and type safe SDKs. You can see the results for yourself: - [Speakeasy's Python SDKs vs. OpenAPI Generator](/docs/languages/python/oss-comparison-python) - [Speakeasy's Typescript SDKs vs. OpenAPI Generator](/docs/languages/typescript/oss-comparison-ts) - [Speakeasy's Go SDKs vs. OpenAPI Generator](/docs/languages/golang/oss-comparison-go)` ## Who Do We Work With? You, we hope! Our product has been battle-tested across 4000 SDKs, and by great product and engineering teams like Shippo, [Airbyte](/post/case-study-airbyte), [Codat](/post/case-study-codat) and more. ## What's next? We're excited to continue helping builders build, by making APIs incredibly easy to create and consume. What's coming up next? - Adding more code generation targets - languages, clients, runtimes, ecosystems, novel targets - Providing plug and play infrastructure to provide deep insights into API changes and usage - Going upstream and integrating into your favorite server side frameworks to remove the need for API specs and cumbersome documentation workflows ## Want to try Speakeasy? Sign up today at [https://www.speakeasy.com/](https://www.speakeasy.com/)! Bring your API spec if you have one - we like those :) ## Talk to us! - Follow us on [Twitter](https://twitter.com/speakeasydev) and [LinkedIn](https://www.linkedin.com/company/speakeasyapi/) - Come chat with us on [Slack](https://go.speakeasy.com/slack) - [Check out our open roles](https://jobs.ashbyhq.com/Speakeasy), we're hiring amazing engineers and a developer focused UX designer. ## Last but not least We'll leave you with a carefully crafted poem. Well not so crafted... thanks ChatGPT! Prompt engineering credit goes to our newest team member, Sterling: > In search of clear instructions, > We turn to the API's functions. > But to use them without confusion, > We need SDK documentation. > Yet sometimes it's hard to follow, > The API's lingo can be hollow. > We need a way to make it clear, > To rewrite it in a language we hold dear. > With OpenGPT-3.5 at our side, > We can translate and simplify with pride. > The developer's task is now made light, > With SDK documentation in their sight. > So let us embrace this new tool, > And make our SDKs easy to use. > From API to SDK, it's a breeze, > With OpenGPT-3.5, we achieve with ease. # Rest of gen.yaml omitted for brevity Source: https://speakeasy.com/blog/introducing-universal-ts import { Callout } from "@/mdx/components"; ## The best SDKs for the biggest language community Today, we're introducing our updated TypeScript code generation target that will power the next wave of TypeScript SDKs built on top of OpenAPI. Our new code generator takes full advantage of TypeScript's type system, native Fetch APIs and the amazing data validation and transformation library [Zod](https://zod.dev/) to deliver feature rich SDKs that run anywhere modern JavaScript can run. There's a lot to unpack so, before going any further, here are the headline features that come with TypeScript SDKs generated using Speakeasy: - Compatibility with the browser & server - Support for popular JavaScript runtimes including Node.js, Bun, Deno, React Native - User input and server response validation with Zod - Support for polymorphic types, also known as unions or `oneOf` in OpenAPI. - Support for multipart streaming upload To get started, all you need is an OpenAPI spec. Simply install the speakeasy CLI, and start generating: ```bash brew install speakeasy-api/tap/speakeasy ``` ```bash speakeasy quickstart ``` If you are currently using Speakeasy for TypeScript generation, we've listed the breaking changes in the later sections of this post as well as instructions on using the new generator. ## New features ### Works in the browser, Node.js and other modern JS runtimes One key decision we took when designing for new TypeScript SDKs was to stick as close to modern and ubiquitous web standards as possible. This included switching from Axios to the Fetch API as our HTTP client. This API includes all the necessary building blocks to make HTTP requests: `fetch`, `Request`, `Response`, `Headers`, `FormData`, `File` and `Blob`. Previously, SDKs leaked some of the Axios API and it meant that users needed to be aware of it which was undesirable. Making this move also ensures that your SDKs will work seamlessly on both the server & browser. We also observed frameworks like `Next.js` which specifically augment the fetch API to enable caching of HTTP responses within React Server Components. That is now unlocked with new SDKs. In addition to browser compatibility, the standard nature of this SDK means it will work in modern JavaScript runtimes. This includes: Node.js, Deno, Bun, React Native. We've already been able to run our extensive suite to confirm that new SDKs work in Node.js, Bun and browsers. We're working to expand our automated testing to cover Deno and React Native. Wherever and however your users are building, they will be able to use your SDK. ### Tree-shaking-ly good Our new SDKs contain fewer internal couplings between modules. This means users that are bundling them into client-side apps can take advantage of better tree-shaking performance when working with "deep" SDKs. These are SDKs that are subdivided into namespaces such as `sdk.comments.create(...)` and `sdk.posts.get(...)`. Importing the top-level SDK will pull in the entire SDK into a client-side bundle even if a small subset of functionality was needed. It's now possible to import the exact namespaces, or "sub-SDKs" as we call them, and tree-shake the rest of the SDK away at build time. ```typescript import { PaymentsSDK } from "@speakeasy/super-sdk/sdk/payments"; // 👆 Only code needed by this SDK is pulled in by bundlers async function run() { const payments = new PaymentsSDK({ authKey: "" }); const result = await payments.list(); console.log(result); } run(); ``` We also benchmarked whether there would be benefits in allowing users to import individual SDK operations but from our testing it seems that this only yielded marginal reduction in bundled code versus importing sub-SDKs. It's highly dependent on how operations are grouped and the depth and breadth of an SDK as defined in the OpenAPI spec. If you think your SDK users could greatly benefit from exporting individual operations then please reach out to us and we can re-evaluate this feature. ### Support for server-sent events We're really excited to share that TypeScript SDKs now support streaming events from your API using [server-sent events][sse] (SSE). SSE is a feature of the web that has been around for quite some time and has seen renewed popularity in the AI space. It's the technology that's powering some of your favourite AI / LLM chat-based user interfaces. Here's an example of working with a chat completion stream in Node.js: ```typescript import { SDK } from "@speakeasy/super-sdk"; async function run() { const sdk = new SDK(); const result = await sdk.chat({ stream: true, messages: [{ role: "user", text: "Tell me three interesting facts about Norwegian Forest cats." }] }); if (!result.chatStream) { throw new Error("expected completion stream"); } for await (const event of result.chatStream) { process.stdout.write(event.data.content); } // 👆 gradually prints the chat response to the terminal } run(); ``` We wanted to make sure the experience is ergonomic and found that exposing an [async iterable][mdn-for-await-of] which can be looped over was our favourite solution. This will work the same way in the browser and other JavaScript runtimes! One of the challenges, we've had to tackle when working on this feature was figuring out how to model these APIs within OpenAPI and we're proud to share that we've developed a proposed specification that is free from propietary extensions. It's vanilla OpenAPI and you can start describing your SSE endpoints with it today then generate SDKs with Speakeasy. As more and more chat-based products emerge, we want to ensure that the APIs and SDKs powering them are free from unnecessary vendor lock-in and instead move towards a common approach to describing them. [mdn-for-await-of]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/for-await...of [sse]: https://html.spec.whatwg.org/multipage/server-sent-events.html ### Runtime validation powered by [Zod](https://zod.dev/) TypeScript provides static type safety to give you greater confidence in the code your shipping. However, TypeScript has limited support to protect from opaque data at the boundaries of your programs. User input and server data coming across the network can circumvent static typing if not correctly modelled. This usually means marking this data as `unknown` and exhaustively sanitizing it. Our new TypeScript SDKs solve this issue neatly by modelling all the data at the boundaries using Zod schemas. That ensures that everything coming from users and servers will work as intended, or fail loudly with clear validation errors. This is even more impactful for the vanilla JavaScript developers using your SDK. ```typescript import { SDK } from "@speakeasy/super-sdk"; async function run() { const sdk = new SDK(); const result = await sdk.products.create({ name: "Fancy pants", price: "ummm" }); } run(); // 🚨 Throws // // ZodError: [ // { // "code": "invalid_type", // "expected": "number", // "received": "string", // "path": [ // "price" // ], // "message": "Expected number, received string" // } // ] ``` While validating user input is considered table stakes for SDKs, it's especially useful to validate server data given the information we have in your OpenAPI spec. This can help detect drift between schema and server and prevent certain runtime issues such as missing response fields or sending incorrect data types. ### Unions are here Support for polymorphic types is critical to most production applications. In OpenAPI, these types are defined using the `oneOf` keyword. We represent these using TypeScript's union notation `Cat | Dog`. We want to give a big shout out to Zod for helping us deliver this feature! ```typescript import { SDK } from "@speakeasy/super-sdk"; async function run() { const sdk = new SDK(); const pet = await sdk.fetchMyPet(); switch (pet.type) { case "cat": console.log(pet.litterType); break; case "dog": console.log(pet.favoriteToy); break; default: // Ensures exhaustive switch statements in TypeScript pet satisfies never; throw new Error(`Unidentified pet type: ${pet.type}`) } } run(); ``` ### Support for data streaming Support for streaming is critical for applications that need to send or receive large amounts of data between client and server without first buffering the data into memory, potentially exhausting this system resource. Uploading a very large file is one use case where streaming can be useful. As an example, in Node.js v20, streaming a large file to a server using an SDK is only a handful of lines: ```typescript import { openAsBlob } from "node:fs"; import { SDK } from "@speakeasy/super-sdk"; async function run() { const sdk = new SDK(); const fileHandle = await openAsBlob("./src/sample.txt"); const result = await sdk.upload({ file: fileHandle }); console.log(result); } run(); ``` On the browser, users would typically select files using `` and the SDK call is identical to the sample code above. Other JavaScript runtimes may have similar native APIs to obtain a web-standards [File](https://developer.mozilla.org/en-US/docs/Web/API/File) or [Blob](https://developer.mozilla.org/en-US/docs/Web/API/Blob) and pass it to SDKs. For response streaming, SDKs expose a [ReadableStream](https://developer.mozilla.org/en-US/docs/Web/API/ReadableStream), a part of the [Streams API](https://developer.mozilla.org/en-US/docs/Web/API/Streams_API) web standard. ```typescript import fs from "node:fs"; import { Writable } from "node:stream"; import { SDK } from "@speakeasy/super-sdk"; async function run() { const sdk = new SDK(); const result = await sdk.usageReports.download("UR123"); const destination = Writable.toWeb( fs.createWriteStream("./report.csv") ); await result.data.pipeTo(destination); } run(); ``` ### Support for arbitrary-precision decimals powered by [decimal.js](https://github.com/MikeMcl/decimal.js) Using decimal types is crucial in certain applications such as code manipulating monetary amounts and in situations where overflow, underflow, or truncation caused by precision loss can lead to significant incidents. To describe a decimal type in OpenAPI, you can use the `format: decimal` keyword. The SDK will take care of serializing and deserializing decimal values under the hood. ```typescript import { SDK } from "@speakeasy/super-sdk"; import { Decimal } from "@speakeasy/super-sdk/types"; const sdk = new SDK(); const result = await sdk.payments.create({ amount: new Decimal(0.1).add(new Decimal(0.2)) }); ``` ### Support for big integers using the native `BigInt` type Similar to decimal types, there are numbers too large to be represented using JavaScript's `Number` type. For this reason, we've introduced support for `BigInt` values. In an OpenAPI schema, fields that are big integers can be modelled as strings with `format: bigint`. ```typescript import { SDK } from "@speakeasy/super-sdk"; const sdk = new SDK(); const result = await sdk.doTheThing({ value: 67_818_454n, value: BigInt("340656901") }); ``` ## Breaking changes ### ES2020 and Node.js v18+ In order to deliver our leanest TypeScript SDKs yet, we set out to avoid unnecessary third-party libraries, polyfills and transpilation which could inflate JavaScript bundles. Based on browser and backend usage statistics, we decided to create a support policy which targets JavaScript features that have been available for at least 3 years. Additionally, when it comes to Node.js in particular, we'll be supporting the current LTS releases. At the time of writing, this is version 18 ([source](https://nodejs.org/en/about/previous-releases)). ### Moving from Axios to fetch Our previous TypeScript SDK generator used [Axios](https://axios-http.com/) as the underlying HTTP client. SDKs were also exposing Axios APIs to users establishing an unwanted expectation that they are familiar with this library and understand how to configure custom clients and requests. Fortunately, the [Fetch API](https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API) is a standard web platform API and has become ubiquitous across all runtimes, from browsers to React Native to Bun/Deno/Node.js. Switching to `fetch` means SDKs no longer pull in an unnecessary third-party dependency and leverage standard platform features. ### Changes in SDK file structure In previous versions of our SDKs, various functionality such as model classes and types were nest in the package's directory structure under a `dist/` folder. While modern editors with language server support take the burden out of typing out imports, it was still unpleasant to see the build folder present in import paths. Many of our existing users commented as much and we fully agree so we've updated how we package SDKs into NPM modules and eliminated this folder from appearing in import paths. This is a breaking change but one that we think SDK owners and users will appreciate. We've also reorganised various supporting files in the SDK and moved away from the `internal/` package to `lib/`. We do not believe this is going to affect end-users of SDKs but, since it's a breaking change, we're listing it here for completeness. ## Next steps If you are using Speakeasy to generate your TypeScript SDK for the first time, then you'll automatically be using our new generator. For existing Speakeasy customers with TypeScript SDKs, we've introduced a new field that you can add in your `gen.yaml` file, called `templateVersion`, to opt-in to the new generator: ```diff configVersion: 1.0.0 typescript: + templateVersion: v2 ``` If you are using our GitHub Action then, after committing that change, the next run will generate a refreshed SDK. `speakeasy` CLI users can rerun the `generate` command which will pick up the flag and regenerate the new SDK. ## Building on good foundations We're really excited to provide users with an awesome experience using machine-generated SDKs. There's often a trade-off that product engineers and API owners consider when relying on code generators versus hand-building SDKs and the quality of the code and public interface they produce. We believe that our refreshed TypeScript SDK generator has baked in a lot of good ideas that ultimately result in a great developer experience, one that increasingly feels like working with a carefully curated TypeScript SDK. We now have the foundation to build even more exciting features like support for Server-sent Events and we're looking forward to taking more of the pain away from shipping awesome DX for your products. If you do try out Speakeasy and our TypeScript SDKs then we'd love to get your feedback about your experience, new ideas or feature requests. Happy hacking! # java-general-availability-and-managed-oauth-support Source: https://speakeasy.com/blog/java-general-availability-and-managed-oauth-support Another changelog, another massive improvement to our SDK generation. This month, it's Java's turn for a GA makeover. But our other languages aren't gathering dust. We've added support for managed OAuth2.0 client credentials flow, and flattened response objects. Let's jump into it 🚀 ## Java General Availability Java is inevitable. You can have your head turned by all the new & exciting languages that are being created, but if you are serving a large organization, you will need to be ready to support Java. Which is why we are excited to announce that Java is now Generally Available on the Speakeasy Platform! General availability means that every feature of our generation platform is now available for Java. As a bonus, we've taken this work as an opportunity to completely revamp the developer experience. Here are the highlights of what's changed: - Enhanced `null` safety and `Optional` support - Builder patterns for better readability, discoverability, and convenient overloads - Lists instead of arrays for collections - No direct field access (getter methods are used now) - A simplified Gradle project structure - Support for non-discriminated `oneOf`s - Auto-Pagination - Retry support - OAuth2.0 support Check out the new interface: ```java public class Application { public static void main(String[] args) { try { MyPlatform sdk = MyPlatform.builder() .security(Security.builder() .authHeader("Basic BASE_64_ENCODED(API_KEY)") .build()) .build(); UserRequestBody req = UserRequestBody.builder() .name("Saj Batchu") .role(RoleType.ADMIN) .workspaces(java.util.List.of( workspaceRef.builder() .id("60d2fa12-8a04-11ee-b9d1-0242ac120002") .build())) .build(); CreateUserResponse res = sdk.user().create() .request(req) .call(); if (res.user().isPresent()) { // handle response } } catch (Exception e) { // handle exception } } } ``` For the details, please read the full announcement [here](/post/release-java-ga). ## 🚢 Improvements and Bug Fixes 🐛 **Based on most recent version: [Speakeasy v1.210.0](https://github.com/speakeasy-api/speakeasy/releases/tag/v1.210.0)** 🚢 Handle flat responses for empty status codes \ 🚢 Fail schema validation if missing a server \ 🚢 Propagate defaults for examples in parameters ### Typescript 🚢 Support Empty Error types \ 🚢 Add support for response formats and flat responses \ 🚢 Add managed OAuth2.0 client credentials flow support (as a SDK hook) \ 🚢 Handle null enums fields and union members \ 🐛 Fix missing imports with flat responses ### Java 🚢 Support passing in additional dependencies in the SDK \ 🚢 Support `callAsStream` autopagination syntax \ 🐛 Fix typo in README.md Error Handling section ### Python 🚢 Add support for response formats and flat responses ### Terraform 🚢 Use the default http client in TF to remove timeouts ### C# 🚢 Support naming servers ### Go 🚢 Add support for response formats and flat responses # java-sdks-telemetry Source: https://speakeasy.com/blog/java-sdks-telemetry In 2022, we've seen the acceleration of a trend: the dispersal of programming language usage. There's no sign of that trend abating anytime soon. That's great for nerding out over language design, but it's a pain if you're on the hook for building your API's client libraries. Whereas 15 years ago you could have covered most programmers with 3-4 libraries, it's now probably closer to 8-10. Language dispersal makes it harder to provide the same level of service to your users and makes it harder to keep track of how users are interfacing with your product. We're committed to giving people the tools they need to manage this new reality. We're constantly working to add more languages to [our SDK generator](/docs/sdks/create-client-sdks/): the alpha for Java is live! And we've baked telemetry into all of the SDKs we create so you can get a clear picture of how your users experience your API. ## New Features **Client SDKs: Java (Alpha)** - Love it or hate it, there's no denying the importance of Java. As one of the most widely-used server-side programming languages, anyone building an API should be sure to make sure they have SDKs for the Java community. We've built our Java SDKs to make sure they follow Java conventions and design patterns to provide a consistent interface for developers interacting with your API: - **Minimal Dependencies**: we use [Jackson Libary](https://github.com/FasterXML/jackson) to (de)serialize models, and use java.net HttpClients to make HTTP requests. - **Annotations for all generated models**: this allows us to append per field metadata to correctly serialize and deserialize models based on the OpenAPI document. - **A utils module**: to contain methods for configuring the SDK and serializing/deserializing the types we generate, to avoid duplication of code in each method reducing the readability. To see the SDKs for yourself, just download our CLI: **_brew install speakeasy-api/tap/speakeasy_** **_speakeasy quickstart_** **SDK Usage Telemetry** - If you don't know which SDKs developers are using to access your API, then you've got a blind spot in your understanding of your users. Github stars offer an imperfect metric: they help you understand interest rather than SDK usage. We've addressed this common gap in telemetry with our SDKs. When you integrate Speakeasy into your API, you will get SDK usage telemetry out of the box! SDK Language and version are now available as filter criteria in the usage dashboard and request viewer, so you can easily get a clear picture of which languages your users favor. ## Small Improvements **New UX** - Same product with a fresh coat of paint. New icons, colors, and navigation for a much improved user experience. You've probably noticed a difference in our latest Gifs, so head over to our app to experience it yourself. # label-based-versioning-openapi-transformations-and-overlay-insights Source: https://speakeasy.com/blog/label-based-versioning-openapi-transformations-and-overlay-insights import { Callout, ReactPlayer } from "@/lib/mdx/components"; Ever wished managing SDK versions was as simple as adding a label? Or wanted your OpenAPI transformations to just work™️ every time you regenerate? We've got you covered with some powerful new features that will make iterating on your SDK a breeze. ## GitHub-Native Version Management
Managing SDK versions should be as natural as any other GitHub workflow. Now it is! With label-based versioning, you can control your SDK's version bumps right from your pull request: - **Automated Version Detection**: By default, we'll analyze your changes and suggest the appropriate semantic version bump. You'll see our suggested version label on your generated PR. - **Manual Override**: Want to override our suggestion? Just remove the current label and add a `major`, `minor`, or `patch` label to your PR. - **Persistent Preferences**: Your chosen version bump persists across regenerations until you change it. - **Pre-release Support**: Planning a beta release? When you are ready to move off your pre-release, simply add the label `graduate`. This feature is automatically active in all SDK generation workflows today. If you would also like generation to kick off immediately after adding a label, just add the following to your GitHub workflow file: ```yaml name: Generate permissions: checks: write contents: write pull-requests: write statuses: write "on": workflow_dispatch: inputs: force: description: Force generation of SDKs type: boolean default: false push_code_samples_only: description: Force push only code samples from SDK generation type: boolean default: false set_version: description: optionally set a specific SDK version type: string pull_request: types: [labeled] schedule: - cron: 0 0 * * * ``` ## OpenAPI Transformations ```yaml filename="workflow.yaml" workflowVersion: 1.0.0 speakeasyVersion: latest sources: my-source: inputs: - location: ./openapi.yaml transformations: - removeUnused: true - filterOperations: operations: getPets, createPet include: true # exclude: true - cleanup: true ``` OpenAPI transformations are now a first-class citizen in your generation workflow. Instead of manually running transforms or building custom pipelines, transformations are automatically applied every time your SDK is generated. Available transforms: - **`filterOperations`**: Include or exclude specific operations from your SDK - **`removeUnused`**: Automatically clean up unused schemas and components - **`cleanup`**: Fix and standardize your OpenAPI spec's formatting The best part? Transformations adapt to your spec changes. For example, if you're filtering to include specific operations, newly added operations matching your filter will automatically flow through to your SDK. ## Overlay Summaries
When applying OpenAPI overlays, it's crucial to understand exactly how they're modifying your spec. Our new overlay summaries provide clear, actionable insights into the changes. These summaries help you: - Quickly validate your overlay changes, - Understand the impact on your API spec, - Debug overlay application issues. --- ## 🐝 New features and bug fixes 🐛 Based on the most recent CLI version: [**Speakeasy v1.404.0**](https://github.com/speakeasy-api/speakeasy/releases/tag/v1.404.0) ### Generation platform 🐝 Feat: improved markdown tables in `README.md` \ 🐝 Feat: `defaultErrorName` config param added to enable custom naming of unhandled API error class \ 🐛 Fix: improved handling of complex allOf schemas that merge multiple types \ 🐛 Fix: remove duplication of error types \ 🐝 Feat: warn users about optional request bodies ### PHP 🐝 Feat: replace JMS serializer with custom serializer for better union support \ 🐝 Feat: handle multiple servers \ 🐛 Fix: ensure PHP compile dependency version matches composer ### Terraform 🐝 Feat: added `default` object support \ 🐝 Feat: new `x-speakeasy-terraform-alias-to` extension for mapping to specific values in an array \ 🐝 Feat: support default empty array in terraform \ 🐛 Fix: prevent compilation errors caused by missing response schemas ### Java 🐝 Feat: support added for `additionalProperties` ### Python 🐛 Fix: Prevent compilation errors on macOS, and if the source code directory changes ### TypeScript 🐝 Feat: allow hooks to trigger retries in TS SDKs # langchain-vs-haystack-api-tools Source: https://speakeasy.com/blog/langchain-vs-haystack-api-tools import { Callout } from "@/mdx/components"; If you're developing an AI application, you're probably considering using APIs to extend its capabilities and integrate it with various services (internal or external). Fortunately, popular AI agent frameworks like [LangChain](https://python.langchain.com/docs/introduction/) and [Haystack](https://haystack.deepset.ai/), make this easy. You can use them to build an AI agent that can process user queries, interact with APIs, and generate responses based on the data retrieved. All you need to start connecting the agent with outside systems is an [OpenAPI document](/openapi). OpenAPI is a standardized way to describe a RESTful API (in JSON/YAML). The AI agents can use this document to understand and interact with the API. In this guide, we'll build an agent with each of these frameworks. The agents will connect with a mock **F1 API** to answer queries about Formula One (F1) race winners, using the API's OpenAPI document to automatically understand which API endpoints to call, what data to send, and how to interpret the responses. When we're done, we'll compare LangChain and Haystack in terms of their features, workflows, and documentation to help you decide which is better suited to your needs. ## What is an agent? An AI agent is an intelligent system that can autonomously perform tasks like decision-making, data retrieval, and interacting with external systems. Think of it as a digital assistant that helps automate complex workflows without needing constant human supervision. ## How do agents and APIs interact, and where does OpenAPI fit in? The [OpenAPI Specification](/openapi) provides a standardized format for describing RESTful APIs that agents can use to understand what endpoints are available, how to authenticate, what data is needed, and what responses to expect. By [generating an OpenAPI document from code](/openapi/frameworks), you can guarantee a correct interface between agents and APIs. This means agents can autonomously work with APIs, reducing the manual work involved in crafting queries and parsing responses. ## Agents vs. LLMs? Just as web frameworks like Flask or FastAPI simplify full-stack development by handling much of the boilerplate for routing, input validation, and middleware, AI agent frameworks do the heavy lifting when working with large language models. Instead of manually writing HTTP requests and parsing JSON responses line by line, you can use an AI framework to quickly get started using structured components. For example, when building the F1 agent in this guide, using a framework like LangChain or Haystack spares you from writing custom code to fetch the winner from an endpoint like `/winners/2024/monaco`. Instead, you can plug in your OpenAPI document, and the framework's existing tools handle the low-level details. This means less time spent wrestling with request formatting and more time focusing on your agent's logic. For a quick, one-off prompt, directly calling OpenAI or Claude might be simpler. But if you're aiming for a production-level system that is reliable and can handle edge cases like LLM refusal (for example, "I can't answer that"), using a framework saves on development time and headaches, much like web frameworks simplify traditional app development. ## What we're building We'll build an F1 race agent that answers users' questions about F1 races to demonstrate how AI can integrate with an API to retrieve real-time or historical data. When prompted with a question, the agent will autonomously call the relevant endpoints, parse results, and deliver meaningful answers. For example, if a user asks, _"Who won the Monaco Grand Prix in 2024?"_, the agent will: 1. Recognize the query and identify the relevant API endpoint, like `/winners/{year}/{race}`, based on the OpenAPI spec. 2. Call the endpoint with the necessary parameters. 3. Return a clear, user-friendly reply based on the API response, for example, _"Charles Leclerc won the Monaco Grand Prix in 2024."_ Here's a diagram of our agent's workflow: ```mermaid flowchart TB A["User"] -- (1) sends query to --> B["Agent"] B -- (2) reads schema from --> C["openapi.yaml"] B -- (3) formulates API request and calls --> D["API Server"] D -- (4) returns response to --> B B -- (5) sends query context to --> E["LLM"] E -- "(6) generates user-friendly reply for" --> B B -- (7) sends answer to --> A D -- (8) generates --> C A:::user A:::user B:::agent B:::agent B:::agent C:::schema C:::schema D:::api D:::api E:::llm classDef user stroke:#EAB308,stroke-width:2px classDef agent stroke:#3B82F6,stroke-width:2px classDef schema stroke:#6366F1,stroke-width:2px classDef api stroke:#F87171,stroke-width:2px classDef llm stroke:#84CC16,stroke-width:2px ``` ## AI frameworks: LangChain and Haystack **LangChain** and **Haystack** are two popular frameworks for API integration, each offering a different design philosophy and user experience. ### LangChain One of the most widely-used frameworks for building AI applications, [LangChain](https://python.langchain.com/docs/introduction/) is known for its ability to integrate with OpenAPI documents, enabling agents to understand and interact with APIs naturally. In theory, this makes LangChain a solid candidate for building our F1 agent, where the goal is to fetch race winners from a FastAPI backend based on user queries. Our expectation was that LangChain would deliver a smooth transition from a static spec file to a working prototype in no time: Feed in an OpenAPI document, and the agent should automatically figure out which endpoint (such as `/winners/{year}/{race}`) to call for queries like "Who won the Monaco Grand Prix in 2024?" In reality, we spent a lot of time struggling with LangChain's documentation. Much of the OpenAPI integration guidance was outdated, referencing methods and classes that had been deprecated or moved elsewhere. Trying to use the code within the Langchain OpenAPI guide often resulted in deprecation errors like this one: ```text LangChainDeprecationWarning: The class `ChatOpenAI` was deprecated in LangChain 0.0.10 and will be removed in 1.0. An updated version of the class exists in the :class:`~langchain-openai package and should be used instead. To use it run `pip install -U :class:`~langchain-openai` and import as `from :class:`~langchain_openai import ChatOpenAI`` ``` This forced us into a trial-and-error approach, where we had to rely on `langchain-community` modules rather than the official `langchain` package. #### Hallucination and debugging the agent Once we had a working prototype, our LangChain-based agent occasionally hallucinated non-existent endpoints (like `/winners/latest`), and required extra prompt tuning and spec details to keep it on track. The silver lining was that the `AgentExecutor` tool allowed us to see the agent's internal reasoning steps so we could trace the logic that led to a faulty endpoint choice. This transparency helped us debug the agent's behavior. #### Strengths - Highly flexible and extensible ecosystem. - Large community offering workarounds and additional tools. - Useful view of the agent's "chain of thought" for debugging and prompt engineering. #### Challenges - Outdated and incomplete official documentation. - Reliance on community-driven modules. ### Haystack [Haystack](https://haystack.deepset.ai/) was initially designed for search, retrieval-augmented generation (RAG), and question-answering (QA), but it's evolved into a general-purpose AI agent framework that also integrates with OpenAPI documents. While this means that Haystack can enable an agent to understand and call specific API endpoints much like LangChain does, it handles such tasks with a more standardized pipeline structure. Haystack's modular pipeline structure uses standardized components to handle different parts of an agent's workflow, making it easier to understand how tasks are processed step by step. Compared to LangChain's "black box" approach, this modularity made the experience of implementing our F1 agent with Haystack more straightforward. Without having to hunt down unofficial modules or rummage through outdated examples, we got the agent calling the correct `/winners/{year}/{race}` endpoint and retrieving data with comparatively little friction. Queries like "Who won the Monaco Grand Prix in 2024?" worked reliably within a much shorter setup time. A standout feature of Haystack is its accurate and up-to-date documentation. Instead of encountering deprecated functions or incomplete references, we found guides that matched the latest version of the framework and provided clear, step-by-step instructions. Unlike LangChain, where we found that most new features required some community-driven workaround, Haystack's official docs led us directly to solutions that work out of the box. #### Hallucination and debugging the agent As with our LangChain-based agent, our Haystack-based agent occasionally tried to call non-existent endpoints. We addressed this by providing more detailed descriptions in the OpenAPI document, as we did previously. Unlike LangChain, Haystack didn't readily expose the agent's reasoning steps. Although this made certain debugging tasks less transparent, the overall reliability of the agent and reduced need for guesswork in its implementation meant we didn't feel as dependent on seeing those internal processes. Haystack's consistent documentation and structured design approach helped us avoid many of the pitfalls that required extensive investigation in LangChain. For example, we asked the question "Who is the latest winner of the Monaco Grand Prix?" The Haystack agent hallucinated an endpoint: ```text HTTP error occurred: 500 Server Error: Internal Server Error for url: http://127.0.0.1:8000//winners/2023/monaco_gp while sending request to http://127.0.0.1:8000//winners/latest/monaco_gp Error invoking OpenAPI endpoint. Error: HTTP error occurred: 500 Server Error: Internal Server Error for url: http://127.0.0.1:8000//winners/2023/monaco_gp LLM Response: It seems that there was a server error when trying to retrieve the information for the latest Monaco Grand Prix. Unfortunately, I cannot access the data at the moment. If you're looking for information about the winners or another specific query regarding F1, please let me know, and I'll do my best to assist you! ``` #### Strengths - Clear, up-to-date documentation. - More stable and consistent components, resulting in fewer breaking changes. - No reliance on community modules. #### Challenges - Smaller ecosystem means fewer experimental or cutting-edge integrations. - No visibility into the agent's internal reasoning steps. ## Building an OpenAPI agent Now we'll walk you through creating two AI agents that interact with an F1 API using OpenAPI documents: One built with LangChain and the other with Haystack. We'll cover setting up the F1 API server, loading the OpenAPI document, and implementing each agent. ### Example API Server The agents use an F1 API built with FastAPI, featuring endpoints for race standings, to demonstrate how agents interact with real-world data. The F1 API includes the endpoints `/winners/{year}/{race}`, which returns the winner of a specific race in a given year, and `/winners/{year}`, which returns all race winners for that year. Take a look at the [F1 API repository](https://github.com/speakeasy-api/openapi-agent-examples/tree/main/f1-fastapi-server) for more details on the API implementation. #### Running the F1 API server To run the F1 API server, follow these steps: 1. Clone the [repository](https://github.com/speakeasy-api/openapi-agent-examples/tree/main/f1-fastapi-server) and navigate to the `f1-fastapi-server` directory. ```bash git clone https://github.com/speakeasy-api/openapi-agent-examples.git cd openapi-agent-examples/f1-fastapi-server ``` 2. Install the required dependencies using `pip install -r requirements.txt`. 3. Run the FastAPI server using `uvicorn main:app --reload`. The API server will start on `http://127.0.0.1:8000/` by default. ### Generating the OpenAPI document We'll use FastAPI's built-in support for OpenAPI to generate the OpenAPI document that describes the F1 API's endpoints, request parameters, and response formats. This spec serves as a blueprint for the AI agent to understand the API's capabilities and interact with it effectively. Find the generated OpenAPI document by spinning up the FastAPI server and navigating to `http://127.0.0.1:8000/openapi.json`. Convert the OpenAPI document to YAML format using an online tool like [JSON to YAML](https://www.json2yaml.com/). ### Building an agent with LangChain You can follow along with the code in our [example repository](https://github.com/speakeasy-api/openapi-agent-examples). #### Prerequisites You'll need the following to build the agent with LangChain: - An Anthropic API key to use with the ChatAnthropic model. Sign up for an account and get an API key from the [Anthropic website](https://www.anthropic.com/). - The OpenAPI document (`openapi.json`) that describes the F1 API's endpoints and data structure. Read the [running the F1 API server](#running-the-f1-api-server) section to generate the OpenAPI document. #### 1. Importing libraries First install and import the necessary libraries for building the agent: ```txt filename="requirements.txt" langchain_anthropic langchain_community ``` ```bash pip install -r requirements.txt ``` Now import the required modules in the agent script: ```python filename="langchain_agent.py" import os import json import argparse from langchain_community.utilities.requests import RequestsWrapper from langchain_community.agent_toolkits.openapi import planner from langchain_community.agent_toolkits.openapi.spec import reduce_openapi_spec from langchain_anthropic import ChatAnthropic ``` #### 2. Checking the API key Let's add a check to ensure the API key is set before proceeding: ```python filename="langchain_agent.py" if "ANTHROPIC_API_KEY" not in os.environ: raise ValueError("ANTHROPIC_API_KEY environment variable not set") ``` #### 3. Parsing command-line arguments We'll use argparse to parse command-line arguments for using the agent: ```python filename="langchain_agent.py" argparser = argparse.ArgumentParser() argparser.add_argument("query", type=str, help="User query. E.g: 'Who won in Monaco in 2024?'") argparser.add_argument("--model", type=str, default="claude-3-sonnet-20240229", help="Model name") argparser.add_argument("--timeout", type=int, default=10, help="Timeout in seconds") argparser.add_argument("--stop", type=str, default="", help="Stop token") args = argparser.parse_args() ``` This code snippet provides a CLI interface to input user queries and set optional parameters like model name, timeout, and stop token. #### 4. Initializing the Anthropic model Next, we'll initialize the Anthropic model for generating responses: ```python filename="langchain_agent.py" model = ChatAnthropic( model_name=args.model, timeout=args.timeout, stop=[args.stop], ) ``` #### 5. Loading the OpenAPI document Now load the OpenAPI document (`openapi.json`): ```python filename="langchain_agent.py" with open("openapi.json") as f: openapi = json.load(f) ``` #### 6. Reducing the OpenAPI document We'll reduce the OpenAPI document to optimize it for the agent: ```python filename="langchain_agent.py" f1_spec = reduce_openapi_spec(openapi) ``` #### 7. Initializing the requests wrapper Now initialize `RequestsWrapper` to handle HTTP requests: ```python filename="langchain_agent.py" requests_wrapper = RequestsWrapper() ``` #### 8. Creating the OpenAPI agent Create the OpenAPI agent by combining the optimized OpenAPI document, request handling, and Anthropic model: ```python filename="langchain_agent.py" f1_agent = planner.create_openapi_agent( f1_spec, requests_wrapper, model, allow_dangerous_requests=True ) ``` Here we set `allow_dangerous_requests` to `True` to enable the agent to make potentially unsafe requests to the F1 API. #### 9. Invoking the agent Finally, invoke the agent with the user query: ```python filename="langchain_agent.py" f1_agent.invoke(args.query) ``` #### Running the script Before running the script, make sure the [F1 API server is running](#running-the-f1-api-server). 1. Place the OpenAPI document (`openapi.json`) in the working directory. 2. Set your Anthropic API key in the environment variables: ```bash export ANTHROPIC_API_KEY="your_anthropic_api_key" ``` 3. Run with a query: ```bash python langchain_agent.py "Who won the Monaco Grand Prix in 2024?" ``` You should receive a response similar to the following: ```text Based on the data from the F1 API, Charles Leclerc won the 2024 Monaco Grand Prix. ``` ### Building an agent with Haystack You can follow along with the code in our [example repository](https://github.com/speakeasy-api/openapi-agent-examples). #### Prerequisites You'll need the following to build an agent with Haystack: - An OpenAI API key. You can sign up for an account and get an API key from the [OpenAI website](https://platform.openai.com/). - The OpenAPI document (`openapi.yaml`) that describes the F1 API's endpoints and data structure. Read the [running the F1 API server](#running-the-f1-api-server) section to generate the OpenAPI document. #### 1. Importing libraries First, install and import the necessary libraries for building the pipeline: ```txt filename="requirements.txt" haystack-ai openapi3 jsonref haystack-experimental==0.3.0 ``` Run the following command to install the required libraries: ```bash pip install -r requirements.txt ``` Now import the required modules in the agent script: ```python filename="haystack_agent.py" import os import argparse from haystack import Pipeline from haystack.dataclasses import ChatMessage from haystack.components.builders import ChatPromptBuilder from haystack.components.generators.chat import OpenAIChatGenerator from haystack_experimental.components.tools.openapi import OpenAPITool, LLMProvider ``` #### 2. Checking the API key Let's add a check to make sure that the API key is set: ```python filename="haystack_agent.py" if "OPENAI_API_KEY" not in os.environ: raise ValueError("OPENAI_API_KEY environment variable not set") ``` #### 3. Initializing the OpenAPI tool Next, initialize the OpenAPI tool with the F1 API schema: ```python filename="haystack_agent.py" f1_tool = OpenAPITool( generator_api=LLMProvider.OPENAI, spec="openapi.yaml", ) ``` #### 4. Building the prompt template Let's define a conversation template to guide the LLM in generating responses: ```python filename="haystack_agent.py" messages = [ ChatMessage.from_system( "Answer the F1 query using the API. Race names with two words should be separated by an underscore and be in lowercase. The API stores data from 2021 to 2024." ), ChatMessage.from_user("User asked: {{user_message}}"), ChatMessage.from_system("API responded: {{service_response}}"), ] builder = ChatPromptBuilder(template=messages) ``` #### 5. Initializing the LLM Set up the LLM to generate API-based replies: ```python filename="haystack_agent.py" llm = OpenAIChatGenerator(generation_kwargs={"max_tokens": 1024}) ``` #### 6. Building the pipeline Now we'll create a pipeline that connects the OpenAPI tool, prompt builder, and LLM. Haystack's modular design allows us to chain components together easily: ```python filename="haystack_agent.py" pipe = Pipeline() pipe.add_component("f1_tool", f1_tool) pipe.add_component("builder", builder) pipe.add_component("llm", llm) pipe.connect("f1_tool.service_response", "builder.service_response") pipe.connect("builder.prompt", "llm.messages") ``` #### 7. Querying the pipeline Define a function to process user queries through the pipeline and return the LLM's response: ```python filename="haystack_agent.py" def query_f1_pipeline(user_query: str): """ Run the F1 bot pipeline with the user's query. :param user_query: The user's query as a string. :return: The response generated by the LLM. """ result = pipe.run(data={ "f1_tool": { "messages": [ChatMessage.from_user(user_query)] }, "builder": { "user_message": ChatMessage.from_user("Answer the F1 query in a user-friendly way") } }) return result["llm"]["replies"][0].content ``` #### 8. Creating a CLI tool Finally, let's set up the main function to allow the script to accept user input via command-line arguments: ```python filename="haystack_agent.py" def main(): parser = argparse.ArgumentParser(description="Query the F1 pipeline CLI tool.") parser.add_argument("query", type=str, help="User query. E.g., 'Who won in Monaco in 2024?'") parser.add_argument("--model", type=str, default="gpt-4", help="Model name") parser.add_argument("--timeout", type=int, default=10, help="Timeout in seconds") parser.add_argument("--max_tokens", type=int, default=1024, help="Maximum tokens for response") args = parser.parse_args() llm.generation_kwargs["max_tokens"] = args.max_tokens response = query_f1_pipeline(args.query) print("LLM Response:", response) if __name__ == "__main__": main() ``` #### Running the script Before running the script, make sure the [F1 API server is running](#running-the-f1-api-server). 1. Place the F1 API document (`openapi.yaml`) in the working directory. 2. Set your OpenAI API key as an environment variable: ```bash export OPENAI_API_KEY="your_openai_api_key" ``` 3. Run the script with a query: ```bash python haystack_agent.py "Who won in Monaco in 2024?" ``` You should receive a response similar to the following: ```text LLM Response: Based on the data from the F1 API, Charles Leclerc won the 2024 Monaco Grand Prix. ``` ## The production gap Turning a proof-of-concept into a production-ready system involves more than just getting basic queries right. Even if your agent works perfectly in testing, production brings new challenges, like how to handle errors, manage resources, and keep the agent secure. ### Error handling In production, your agent must gracefully handle unexpected inputs, outdated OpenAPI documents, and API timeouts. If a user asks about a non-existent race, your agent should respond with a helpful message rather than crash. Input validation, API timeout handling, and clear error messages ensure the system stays stable. Logging errors makes it easier to troubleshoot issues later. ### Resource management As usage grows, so do costs and performance demands. Without limits, frequent LLM and API calls can lead to significant bills. Caching popular results reduces unnecessary calls. Monitoring request rates and scaling resources as needed helps maintain responsiveness without overspending. ### Security With increased traffic comes higher security risks. Always sanitize user inputs to prevent malicious payloads. Secure API keys, add authentication if needed, and ensure only authorized users can access sensitive endpoints. ### Making your agent production-ready Implement rate limiting to handle traffic spikes, and add retries for flaky network calls. A good monitoring system should warn you if something goes wrong, while a thorough testing suite (including integration tests) ensures that updates don't break critical features. ## Final thoughts LangChain is more of a collection of tools than a fully integrated framework. The LangChain toolset offers good flexibility, allowing you to pick and choose components, integrate various LLMs, and adapt the setup to unusual requirements. However, LangChain's official documentation is lacking, and you'll rely on community modules to piece together solutions. Developing a good agent involves experimenting with `langchain-community` modules to replace outdated instructions. Haystack, on the other hand, focuses on reliability and production-readiness. The Haystack documentation is clear and up-to-date, and the structured pipeline design makes it easier to understand how components interact. Haystack also offers a lower risk of breaking changes and deprecated functions, and eliminates the need to rely on community modules to fill gaps. Keep in mind that even with the right framework, AI agents can hallucinate endpoints and produce unexpected errors. Overall, Haystack is the better choice for production-level systems and quick POCs, while LangChain is more suited for projects that require greater flexibility and allow for time to experiment. # last-changelog-of-the-year-looking-ahead-and-a-thank-you Source: https://speakeasy.com/blog/last-changelog-of-the-year-looking-ahead-and-a-thank-you import { Callout } from "@/lib/mdx/components"; ## 2024, a year in review 📅 As we wrap up 2024, I wanted to take a moment to reflect on the year. It's been a year of growth, learning and building. We've launched some exciting new features, expanded our team and continued to push the boundaries of what's possible with code generation. Here are some of the highlights from 2024: - We continued the march towards our vision of "generating handwritten SDKs" for REST APIs. Our TypeScript generation led the charge adding [functional support](/post/standalone-functions), [react hooks](/post/release-react-hooks) and [webhooks](/post/release-webhooks-support). Other languages followed with [PHP adding support for Laravel](/post/release-php) and Python with [async functions and Pydantic2.0 type-safety](/post/release-python-v2-alpha). - We launched our [API contract testing](/post/release-contract-testing/) feature to help you automate your API testing leveraging the completeness of SDKs. - We brought on several new team members across engineering, support and GTM. This team is on 🔥. - We raised a [Series A](/post/fundraising-series-a/) to fuel our growth and expand our product offerings! ## 2025, what's coming up? 🚀 As I look ahead to 2025 the API landscape is rapidly evolving for consumers with AI agents and LLMs changing how we manage and execute API calls. I'm absolutely thrilled for what we're cooking behind the scenes. Here's a sneak peek at what's coming up in 2025: - An exciting new way to design your SDKs ! 🎨 - Expanding on [our initial foray into API testing]() with more Arazzo support, multi endpoint testing and a new testing experience. - Support for more non-HTTP protocols: webhooks, events and internal API landscapes - Make it really easy for LLMs to discover and connect with your APIs. All your AI tools, generated. - Leverage the power of executable code samples through prompts. No more 3 pane doc sites. Simply put, the job's not finished ! 🚧 ![The job's not finished](/assets/changelog/assets/jobs_not_finished.gif) ## A Thank You ! 🎉 Finally we'd love to thank you all for making this year a success. We're excited to continue building the future of APIs in 2025. We're so grateful for your continued support. 🙏 A few special shoutouts: ### To our Customers Thank you for your invaluable feedback and for trusting us with your API needs. You help us push the bar everyday! ### To our Team Thank you for your hard work and dedication. I'm grateful to be surrounded a very talented group of folks who are relentless. This team cooks! ### To our Partners Thank you for your collaboration and support. - Colin McDonnell for continuing to push Zod forward. We're lucky to build on the shoulders of giants. - The team at Vercel for driving us towards functional and performance-optimized SDKs. - The Pydantic team for prioritizing fixes and improvements in Pydantic2.0. - Marc and Scalar Team for iterating with us as product partner. Scalar Docs + Speakeasy SDKs is a game changer! - Our amazing blog contributors: The team at Ritza, Phil Sturgeon, Steve McDougall and many more. - Paul, Flo and the folks at Supabase for bringing some of the best dev tools together for Mega Launch Week. ### To our Investors Thank you for backing us and supporting our vision 🚀 ## 🐝 New features and bug fixes 🐛 Based on the most recent CLI version: [**v1.460.3**](https://github.com/speakeasy-api/speakeasy/releases/tag/v1.460.3) ### Generation platform 🐝 feat: added a new `normalise` transform to available OpenAPI `transforms` \ 🐛 fix: improved handling of complex allOf schemas that merge multiple types \ 🐛 fix: validate custom linting rulesets before execution ## API testing 🐝 feat: Mock API server automatically generated with test generation \ 🐛 fix: improvement to result variable names in tests \ 🐝 feat: add simple support for multiple operations in contract tests ## TypeScript 🐛 fix extraneous HTTP Authorization header in TypeScript \ 🐝 feat: support for custom webhook security in TS \ 🐛 fix: flattening with hidden parameters and allow server urls to allows be overridable per-method \ 🐛 fix: improve typescriptv2 readme errors \ 🐛 fix: make React hooks peer dependencies optional ## PHP 🐝 feat: support for retries in PHP \ 🐝 feat: support sdk hooks for custom code ## Java 🐝 feat: open enum support \ ## Ruby 🐛 fix: ruby rubocop linting issues \ ## Python 🐛 fix: flattening with hidden parameters and allow server urls to allows be overridable per-method \ 🐝 feat: support for injecting headers \ 🐛 fix: python usage snippet imports and handle property names made up of illegal characters only \ 🐛 fix: avoid unnecessary content-type header in empty python requests ## Golang 🐛 fix: flattening with hidden parameters and allow server urls to allows be overridable per-method \ 🐛 fix: Support go target oneOf types in deepObject style query parameters \ 🐝 feat: support for injecting headers ## Terraform 🐛 fix: bugs with missing terraform types \ 🐛 fix: make useragent better in terraform \ 🐝 feat: add option for disabling deduplication of types # launch-week-0-round-up-webhooks-react-query-support-more Source: https://speakeasy.com/blog/launch-week-0-round-up-webhooks-react-query-support-more Last week we took a break from our normal changelog schedule. We instead joined Supabase and other outstanding dev tool companies for [Mega Launch Week](https://launchweek.dev/lw/2024/MEGA) ⚡ Over the course of 5 days, we launched 5 new products / features for you to get your hands on. Below is the summary for those of you who weren't following along live. If you're interested in any of the features, send us a slack message and we can help you get set up! ## Day 1 - API contract testing ![API contract testing](/assets/changelog/assets/day1-testing.png) We're fully automating API testing. Use the Speakeasy platform to generate comprehensive test suites using your favorite testing framework (Vitest, pytest, etc.) with AI-generated test data to cover every test case. [Read the release →](/post/release-contract-testing) ## Day 2 - SDK generation with webhooks support ![SDK generation with webhooks support](/assets/changelog/assets/day2-webhooks.png) We've added webhook support to our SDK generation platform. The new feature provides type-safe webhook handlers and built-in security verification, all powered by native OpenAPI support. [Read the release →](/post/release-webhooks-support) ## Day 3 - Speakeasy API docs ![Speakeasy docs powered by Scalar](/assets/changelog/assets/day3-scalar.png) Scalar's best-in-class documentation platform is now seamlessly integrated into the Speakeasy platform. Generate beautiful, branded documentation that stay in-sync with your SDKs. [Read the release →](/post/release-speakeasy-docs) ## Day 4 - Enhanced PHP generation ![Enhanced PHP generation](/assets/changelog/assets/day4-php.png) We're delivering a whole batch of new features for PHP generation. In addition to Laravel service provider creation, we've added support for pagination, Oauth 2.0 and hooks for custom code. [Read the release →](/post/release-php) ## Day 5 - React query hook generation ![React query hook generation](/assets/changelog/assets/day5-react.png) We've added React hooks to our TypeScript SDK generation platform. The new feature provides type-safe hooks for all your API operations, powered by Tanstack Query. [Read the release →](/post/release-react-hooks) --- You'll be hearing more about these new features in coming changelogs, so stay tuned! # lean-typescript-sdks-for-the-browser Source: https://speakeasy.com/blog/lean-typescript-sdks-for-the-browser import { Callout, CodeWithTabs } from "@/lib/mdx/components"; What's better than an ergonomic, type-safe TypeScript SDK? A **lean**, ergonomic, type-safe TypeScript SDK that's optimized for the browser! We've made some major improvements to our TypeScript generation so that you can better serve your users who are building web applications. And best of all, no breaking changes are required on your end. Just rerun your TypeScript generation and get the latest and greatest 🎉 ## Lean TypeScript SDKs with standalone functions When SDKs are large, they can bloat the bundle size of your user's app, even if they're only using a small portion of your SDK's functionality. That matters because the smaller the bundle, the faster the app loads, the better the user experience. If your SDK is too large, it may be a non-starter for your users. To address this, we've introduced a new feature for TypeScript SDKs called **Standalone Functions** to make your SDKs tree-shakable. That will allow your users to import only the functions they need, resulting in a smaller bundle size and a more performant application. Here's an example of standalone functions in action via [Dub's SDK](https://github.com/dubinc/dub-ts): The performance benefits are enormous. In a benchmark test using a single function from the Dub SDK, bundle-size reduced from **324.4 kb -> 82.1 kb**. That's a **75% reduction** in bundle size! If you want the juicy technical details, check out the [full blog post](/post/standalone-functions). --- ## Regenerate Github SDKs from the CLI ![Speakeasy run github](/assets/changelog/assets/github-run-cli.png) You can now remotely generate your SDKs on GitHub directly from your terminal! Just use the `--github` flag with `speakeasy run` to kick off a remote generation. If you haven't installed the GitHub app yet, don't worry. Follow the prompts in your workspace to complete the setup. This will ensure the Speakeasy Github app has access to your managed repositories within your organization. Ready to streamline your workflow? Give it a try! ## 🐝 New features and bug fixes 🐛 Based on the most recent CLI version: [**Speakeasy v1.388.0**](https://github.com/speakeasy-api/speakeasy/releases/tag/v1.388.0) ### Generation platform 🐝 Feat: Add Summary and ToC sections to Readme generation ### TypeScript 🐝 Feat: support added for custom compile commands \ 🐝 Feat: support for streaming file uploads in test generation \ 🐛 Fix: pagination when used alongside error handling ### Python 🐛 Fix: pagination when used alongside error handling \ 🐛 Fix: correct implementation of unions of arrays \ 🐛 Fix: add errorUnions handling to usage snippets ### Go 🐝 Feat: Support added for streaming uploads ### C# 🐛 Fix: prevent conflicts with `System` namespace ### PHP 🐛 Fix: handle empty global parameters array # loom-for-remote-collaboration Source: https://speakeasy.com/blog/loom-for-remote-collaboration Working on a remote-first dev team can be challenging. Most of us devs have spent years building up processes optimized for in person collaboration. We've found a really quick win has been posting short [Loom](https://www.linkedin.com/company/useloom/) videos for dev updates in our slack! This builds shared context for our team without more meetings. Our hope is to build up dozens of these so new hires can have a library of resources to watch and reference when they join the team. In this short clip, I demonstrate how we use [Pulumi](https://www.linkedin.com/company/pulumi/) for managing infra. # making-gram-ai-friendly Source: https://speakeasy.com/blog/making-gram-ai-friendly import { CalloutCta } from "@/components/callout-cta"; import { GithubIcon } from "@/assets/svg/social/github"; } title="Gram OSS Repository" description="Check out Github to see how it works under the hood, contribute improvements, or adapt it for your own use cases. Give us a star!" buttonText="View on GitHub" buttonHref="https://github.com/speakeasy-api/gram" /> When we started building [Gram](https://github.com/speakeasy-api/gram), our goal wasn't specifically to make the codebase "AI-friendly." Initially, we just wanted a codebase where any developer could jump in and be productive quickly, no tribal knowledge required. Turns out, what makes a codebase easy for new developers also makes it easy for AI agents. ## Think of AI as a developer with broad experience, but shallow context" The best mental model we've found: AI coding agents are developers with a breadth of experience who lack knowledge depth, specifically in the context of your project. They excel at pattern matching but they don't build on context overtime the same way a developer would. This reframe changes how you think about structuring code. If a new developer would struggle to find where API schemas are defined or how to generate migrations, a coding agent will too. ## Contract-first, generated code everywhere The core principle behind Gram's structure is contract-first design with extensive code generation. We use: - [Goa](https://goa.design/) for API contracts and server stub generation - [SQLC](https://sqlc.dev/) for type-safe database queries - [Speakeasy](https://www.speakeasy.com/) for SDK generation - [Atlas](https://atlasgo.io/) for database migrations Here's what that looks like in practice. To add a new API endpoint, you define it in the [design package](https://github.com/speakeasy-api/gram/blob/main/server/design/projects/design.go): ```go var _ = Service("projects", func() { Description("Manages projects in Gram.") Method("listProjects", func() { Description("List all projects for an organization.") Payload(ListProjectsPayload) Result(ListProjectsResult) HTTP(func() { GET("/rpc/projects.list") Param("organization_id") }) Meta("openapi:operationId", "listProjects") Meta("openapi:extension:x-speakeasy-name-override", "list") Meta("openapi:extension:x-speakeasy-react-hook", `{"name": "ListProjects"}`) }) }) ``` Run `mise gen:goa-server` to generate the server stubs and OpenAPI spec. For the database, it works similarly. When adding a new database table, you write a schema in [`server/database/schema.sql`](https://github.com/speakeasy-api/gram/blob/main/server/database/schema.sql): ```sql CREATE TABLE IF NOT EXISTS projects ( id uuid NOT NULL DEFAULT generate_uuidv7(), name TEXT NOT NULL CHECK (name <> '' AND CHAR_LENGTH(name) <= 40), slug TEXT NOT NULL CHECK (slug <> '' AND CHAR_LENGTH(slug) <= 40), organization_id TEXT NOT NULL, created_at timestamptz NOT NULL DEFAULT clock_timestamp(), updated_at timestamptz NOT NULL DEFAULT clock_timestamp(), deleted_at timestamptz, deleted boolean NOT NULL GENERATED ALWAYS AS (deleted_at IS NOT NULL) stored, CONSTRAINT projects_pkey PRIMARY KEY (id) ); ``` Instead of writing migration files manually, run `mise db:diff add_organization_metadata` and Atlas generates the migration SQL for you. Then write your queries using SQLC syntax in module-specific [`queries.sql`](https://github.com/speakeasy-api/gram/blob/main/server/internal/projects/queries.sql) files: ```sql -- name: ListProjectsByOrganization :many SELECT * FROM projects WHERE organization_id = @organization_id AND deleted IS FALSE ORDER BY id ASC; ``` Run `mise gen:sqlc-server` and SQLC generates type-safe Go repository that you can connect to your server implementation. No room for deviation, no need to look at five different examples to figure out "the right way." Lastly, running `mise gen:sdk` updates the Speakeasy SDK, complete with React Query hooks for use in the client. ## Mise: A recipe book for agents [Mise](https://mise.jdx.dev/) is a polyglot development environment manager and task runner. Beyond managing tool versions (Go, Node.js, pnpm), it provides a discoverable task system that has become central to how we work. All our development tasks live in [`.mise-tasks`](https://github.com/speakeasy-api/gram/tree/main/.mise-tasks) as executable scripts organized hierarchically: ```bash .mise-tasks/ ├── db/ │ ├── diff.sh # Generate migrations │ ├── migrate.sh # Run migrations │ └── rewind.mts # Rewind migrations ├── gen/ │ ├── goa-server.sh # Generate Goa code │ └── sqlc-server.sh # Generate SQLC code └── start/ ├── server.sh # Start server └── worker.sh # Start worker ``` Agents can run `mise tasks` to see what's available, then execute `mise db:migrate` or `mise gen:sqlc-server`. Progressive disclosure at its best, the agent discovers commands as needed rather than needing everything upfront. ## Everything in one place: The monorepo advantage Gram is a true monorepo: server, web app, CLI, and our NPM functions framework all live together in [`speakeasy-api/gram`](https://github.com/speakeasy-api/gram). This matters because when an agent needs to make changes that touch multiple surfaces (say, adding a new CLI feature that calls a server endpoint), it can see both sides of the interaction. No context switching between repos, no hunting for the right version of a shared type. ## Zero to productive in one command We have a `mise zero` command that sets up everything: installs dependencies, pulls Docker images, starts local services. One command, and you're ready to develop. This isn't revolutionary, it's just thoughtful automation. But it eliminates a whole class of "getting started" friction that would otherwise require back-and-forth with an agent or digging through README files. ## Keep agent instructions minimal We maintain a [`CLAUDE.md`](https://github.com/speakeasy-api/gram/blob/main/CLAUDE.md) (symlinked to `AGENTS.md`, `GEMINI.md`, etc.) with basic onboarding info. But we keep it lean. Rather than documenting every workflow, we point to general patterns and where a developer would find more information: ```markdown ## Available commands All development commands are in Mise. Run `mise tasks` to see available commands, or execute them directly: - `mise db:*` - Database operations - `mise gen:*` - Code generation - `mise start:*` - Start services locally ``` The goal is day-one context, that a developer or a coding agent can explore from there. ## What works well, what doesn't AI agents are great at: - Formulaic work (adding new API endpoints, database tables) - Bug investigation when you can describe the general area of the problem - Changes that generally are heavy on pattern matching They struggle with: - Designing net new patterns or architectures - Understanding subtle business logic without extensive context - Making opinionated decisions about design Our approach: let AI handle the boilerplate, while developers own the design and review. For simple things we describe the structure we want, and the agent writes it out faster than we could. This saves us time to work on the complex. ## The real win Building Gram this way didn't just make it easier to use AI. It made the codebase easier to work with, period. New team members ramp up faster. Code reviews focus on logic, not style. Everyone benefits. If you're building a new project and want it to work well with AI tools, don't overthink it. Focus on clear patterns, good discoverability, and minimizing cognitive load. The AI-friendliness will follow. --- Want to see these principles in action? Check out the [Gram repo](https://github.com/speakeasy-api/gram) or try building an MCP server with [Gram Functions](/docs/gram/getting-started/typescript). # mintlify-integration-plan-validators-and-python-async-beta Source: https://speakeasy.com/blog/mintlify-integration-plan-validators-and-python-async-beta import { Callout, ReactPlayer } from "@/lib/mdx/components"; Generation targets grow up so fast. You announce their alpha release, and before you know it, they're off to beta... And it's not only the Python Generator that's maturing. The Mintlify integration is now self-serve, and Terraform Generation just got even more fully featured with the addition of OpenAPI-based Plan Validators. Read on for the details! ## Mintlify Integration - Now Self-Serve
Making your API documentation SDK-based is easier than ever with our Mintlify integration now available for self-serve. 1. Select the SDKs you want to include in your docs. 2. Point Speakeasy workflow at your Mintlify repo. That's it! Now every new generation of your SDKs will automatically update your Mintlify repo. --- ## Terraform Plan Validators ```go func (r *userResource) Schema(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) { resp.Schema = schema.Schema{ Attributes: map[string]schema.Attribute{ "username": stringattribute.String{ Required: true, Description: "The username of the user.", Validators: []validator.String{ stringvalidator.LengthBetween(6, 64), stringvalidator.RegexMatches( regexp.MustCompile(`^[a-z]+$`), "must contain only lowercase alphanumeric characters", ), }, }, }, } } ``` With the latest Speakeasy release, Terraform Provider generation will now automatically convert additional OAS validation properties into Terraform configuration validators. This will ensure that Terraform users will receive upfront feedback about invalid configurations before they are applied. Automatic OAS -> Terraform validation now includes: - For `string` types: `maxLength`, `minLength`, and `pattern` - For `integer` types: `maximum` and `minimum` - For `array` types: `maxItems`, `minItems`, and `uniqueItems` Refer to [the docs](/docs/terraform/customize/validation-dependencies) for more on validation capabilities. ## Python Beta Release: Pydantic & Async
Last changelog we announced the alpha release of our new Python Generator with support for Async & Pydantic models. We're now excited to announce the new generator is in beta! All new SDKs will use the new generator by default. Existing production SDKs will be migrated by request. For all the details on the new generator, read about [our Python SDK design](/docs/sdk-design/python/methodology-python) ## 🐝 New Features and Bug Fixes 🐛 Based on the most recent CLI version: [**Speakeasy v1.345.0**](https://github.com/speakeasy-api/openapi-generation/releases/tag/v1.345.0) ### The Platform 🐝 Feat: SSE Sentinel - enables API builders to specify a sentinel which indicates that a streaming response has no more content to send. \ 🐝 Feat: `deepObject` style Params now supported \ 🐛 Fix: Optional fields for error models are correctly marked. ### TypeScript 🐛 Fix: Handle renamed object fields using `x-speakeasy-name-override` ### Python 🐝 Feat: Make Python `unset` falsy \ 🐝 Feat: Support defaults and usage snippets for flattened request bodies \ 🐛 Fix: Fix handling of single member unions \ 🐛 Fix: Allow model\_ prefixes on model fields \ 🐛 Fix: Handle renamed object fields using `x-speakeasy-name-override` \ 🐛 Fix: Added support for string unions ### Terraform 🐝 Feat: Plan modifiers created automatically from OpenAPI attributes \ 🐝 Feat: Move custom plan modifiers to the same folder as the normal plan modifiers ### C# 🐝 Feat: Improve `NuGet` metadata ### Go 🐝 Feat: Add support for populating globals from env variables # more-apis-less-complexity Source: https://speakeasy.com/blog/more-apis-less-complexity _This post is the first from the Speakeasy blog! Follow us as we explore why APIs are important to the future of software, the challenges developers at non-FAANG companies face, and what the future of dev infra for APIs holds._ It has always been the case that there are far more ideas in the world which deserve to be built, than there are people to build them. The tricky thing about ideas is that it's really hard to know whether they're good or not until you get knee deep in them. You could of course try and get really good at identifying the ideas that have legs, but the far safer method is to just throw as many ideas at the wall as possible and see what sticks. The question then becomes, as a community, how do we make sure that we are pursuing as many ideas as possible? The answer is that we do what humans have always done, we build tools. The best tools empower each individual to accomplish what previously took 10 people to do. With the right tools the same number of people can collectively tackle 10x the number of projects and thus push the boundaries of what's possible. Nowhere has this pattern of tools providing 10X productivity boosts been more powerful than in software. Free of nearly all physical constraints, the creation and proliferation of software is only ever constrained by the time of the world's developers. Unfortunately, that's still a big constraint. There aren't nearly enough new developers being trained to keep pace with the cambrian explosion of new software products being created. The only way to keep the industry accelerating is to keep building tools which deliver a step change boost in productivity. We believe the next step change in productivity will be achieved when developers' are able to easily operationalise a common, but underutilized technology, APIs. > _If we want to massively accelerate the creation of new products and businesses, we need to make it easier for developers to build, maintain, and ultimately,_ **_use_** _APIs._ APIs are the next solution to the consistent problem of limited developer bandwidth. A well built API is like giving a developer [superpowers](https://www.notboring.co/p/apis-all-the-way-down?s=r). They enable a dev, with a single line of code, to harness functionality that would've taken a team of 10 devs years to build. That dev is now able to focus more time on true innovation rather than reinventing the wheel. A perfect example of this is the explosion of D2C e-commerce, none of which would have been possible without the layers of abstraction in payments processing provided by API companies like [Shopify](https://shopify.dev/docs/api) and [Stripe](https://stripe.com/docs/api): ![A group of astronauts standing next to each other.](/assets/blog/more-apis-less-complexity/more-apis-less-complexity-image-01.png) e-commerce = Abstraction all the way down APIs have the potential to bring an explosion in innovation to every industry they touch. ## Too often, APIs go unbuilt and unused There's no doubt that APIs are the answer to the problem of developer bandwidth. And yet, within the answer lies another set of problems. Too often, APIs go unbuilt because productionizing them is too tedious to be valuable. And even more often, APIs that are built go unused because their value is hidden by a layer of usability issues. Before the full promise of APIs can be realized, these issues need to be fixed. Nearly every development team outside of FAANG (now christened _MAMAA?_) finds itself trapped in what could be described as API development purgatory, they know they need APIs to power innovation across their business, but they can't justify the upfront investment which would be required to build the tooling devs need to easily build said APIs. So, API development limps forward, too important to give up, too tangential to business objectives to be a priority. ## The Problem: Building reliable and usable APIs takes a lot of effort. Without tooling to make development easy, it's each developer's personal conviction that determines whether an API is built, and if so, to what standard. Often developers simply can't justify the time investment which would be required to make the API high quality. This is because writing code is merely one task on the long laundry list of necessary activities which comprise the full API development lifecycle. Some of the most particularly painful problems associated with the API lifecycle are: - **Devs deserve a great user experience too!**: It is extremely easy for static artifacts to get out of sync with code. Every developer has experienced the pain & confusion caused by sparse, stale documentation. - **The dreaded back compat:** It is expensive to maintain multiple versions of an API, but without versioning, making major changes becomes an epic migration with all the associated spreadsheets and client calls. - **Yet another integration**: the API ecosystem is fragmented for Authentication, Gateways, Billing and much more. It's challenging for a developer to keep these all in sync without causing customer downtime and rarely get accounted upfront in development planning. - **K8s Apps, services, ingresses, proxies, gateways…. :(** : The cloud is powerful, but complicated with many moving parts needed for a simple API. Making sure that infrastructure is optimally configured for your use case can take as long as building the product in the first place. The above is really just the tip of the iceberg. For any given API you may also need to consider key management, rate limiting, discovery, dev portals, usage tracking, billing, observability…. When you consider all this, it's a wonder that any APIs get built at all. ![Live Footage of a dev deploying their API to prod](/assets/blog/more-apis-less-complexity/more-apis-less-complexity-image-02.png) ## How do we end API purgatory? Unfortunately, it's not going to happen in a day, but there is hope that the ease of API development can be massively improved for developers. In future posts, we plan to break down each of the individual issues we mentioned and propose what a developer-first solution to the problem might look like. We know we don't have all the answers, so we look forward to hearing from everyone! Please don't hesitate to chime in with your perspective on the challenges facing devs working on APIs, and what you think constitutes a good solution. If you ever want to speak with us, [don't hesitate to reach out!](https://calendly.com/d/5dm-wvm-2mx/chat-with-speakeasy-team) # nivo-vs-recharts Source: https://speakeasy.com/blog/nivo-vs-recharts If you work in B2B it's inevitable that you'll be asked to build a dashboard. Building dashboards means buildings charts, and creating nice-looking charts for the web is undeniably difficult. Sure you _could_ build your own solution directly using D3, but do you really _need_ (or want) to? Probably not. Building off of D3 might allow you to build some elaborate custom visualizations, but as any frequenter of [/r/dataisbeautiful](https://www.reddit.com/r/dataisbeautiful/) knows, simplicity and usability go hand in hand when visualizing data. Good news!--there are plenty of great React libraries for making simple charts. At Speakeasy, we were recently building out an API usage dashboard and in doing so explored Nivo and Recharts (an admittedly non-exhaustive dive into the sea of React charting libraries out there). We want to share, and save you from needing to do the same investigation, at least into those two. ## TLDR If you're picking between Nivo and Recharts, we strongly recommend choosing Recharts. If you're deciding between Recharts and something else, maybe use Recharts (we like it well enough) but also know that it's not without issues. ## Nivo - What We Liked [Nivo](https://nivo.rocks/) is beautiful. Their charts are beautiful, their website is beautiful, their online sandbox is a fantastic feature. All of these are wonderful selling points and will hopefully make for a top-of-the-line chart library one day. We were drawn in by the siren's song and pulled the trigger on it without doing any due diligence investigating other options. Unfortunately there are plenty of problems we encountered (fortunately very quickly). ## Nivo - What We Disliked ### Poor responsiveness Static charts are ugly. Responsiveness is important. Here's what happens when you try to make your Nivo charts responsive: This is a known issue ([here's](https://github.com/plouc/nivo/issues/109) one of several Github issues calling it out). Workarounds exist for certain situations, but for others, no such luck. We were not able to get simple width responsiveness working; this was a big strike against Nivo. ### Poor interface ![A screenshot of a code editor containing a Nivo component.](/assets/blog/nivo-vs-recharts/nivo-vs-recharts-image-02.png) We found Nivo to be very verbose. Those ultra-minimal charts with no axes, gridlines, or tooltips in the gif above? The code for the corresponding Nivo component, with all of its input props, is 30 lines long. To the left is (some of) the code for the default example on [their website](https://nivo.rocks/line/) (the rest of it didn't fit on one screen for a screenshot). You can see that everything is crammed into the component props. Not the end of the world, but as you'll see in the Recharts section, there's a much better way. ### Subpar Technical Documentation The poor interface mostly becomes an issue when paired with subpar documentation. If you want to do something simple (or something that happens to have a corresponding example in their [storybook](https://nivo.rocks/storybook/)), it's (relatively) smooth sailing. If you want to do something that has no example, you'll have to fumble in the dark a bit. Many of the property objects are not documented anywhere, leaving you to guess at what properties are available and what they do. I found myself messing around in their sandbox, looking at the corresponding code, and copying over the relevant properties in a tedious trial & error process. If your use case is simple you'll be okay, but the second you run into an issue, the limited documentation makes troubleshooting very difficult. ### Mindshare is low While very polished-looking, Nivo is one of the least used of the popular React charting libraries. This naturally means mindshare (read: stack overflow answers) will be limited and troubleshooting will involve praying that (a) there's a corresponding issue on Github and (b) someone has responded to that issue with a fix or workaround. And again, because usage is low, that's not super likely. ## Recharts - What We Liked After struggling with the aforementioned issues with Nivo, we decided to cut our losses and swap in a different chart library. After spiking out an implementation using Recharts, we decided it met our needs and was a substantial improvement, so we trialed it everywhere. ### It's easy to make pretty charts The charts are nice-looking (though simple) out of the box. They come with things like a nice animation when the page is loaded It's also trivial to add things like synchronized tooltips. This was a one-line change that yielded pretty cool results. ### It has a nice interface ![Recharts interface](/assets/blog/nivo-vs-recharts/nivo-vs-recharts-image-05.png) The interface for Recharts is very well designed, especially when compared to Nivo. Below is our implementation of the ultra-minimal charts shown above. ![Recharts ultra-minimal charts implementation](/assets/blog/nivo-vs-recharts/nivo-vs-recharts-image-06.png) Declaring the different elements of a chart (axes, gridlines, the line itself) as individual components is very intuitive and nice to use. For the charts above we wanted _just_ the line, so to achieve that we simply added _just_ the \`Line\` component. Beautiful. ## Recharts - What We Dislike ### It has many open issues Here's one annoying one we encountered. Here you can see in that same synchronized tooltip example the page jumps around because the bottom tooltip starts off the screen. This is actually an issue for every tooltip, but most of the time if you hover over a chart it's already on your screen. It was just particularly noticeable for these synchronized charts since the bottom one is often _not_ already on your screen. We tried to no avail to fix it, and ended up having to disable the synchronized tooltips altogether. There are many such issues ([430](https://github.com/recharts/recharts/issues) of them, to be exact) so you'll likely encounter one or two, which can be frustrating given that the documentation is not stellar. ### The documentation has room for improvement While somewhat better than Nivo on the technical side of the docs, Recharts' docs still lack real depth and meat. There are a fair few [examples](https://recharts.org/en-US/examples) which, like Nivo, are helpful if you are trying to do something simple with minimal customization. There's also a [technical reference](https://recharts.org/en-US/api) which is at least a step up over Nivo in that it exists, but also contains so little detail that it's not very useful. It can help remind you what properties exist, but it won't help you figure out anything deeper than that. ## Conclusion In the grand scheme of things, our charting needs are relatively simple and Nivo was not able to meet those simple needs, while Recharts was. So Recharts is definitely worthy of consideration, but we know that isn't the full story. It's definitely worth considering other libraries before you commit. We would love to hear what other libraries people have tried and how they compare to Recharts. # npm-trusted-publishing-security Source: https://speakeasy.com/blog/npm-trusted-publishing-security import { Callout } from "@/lib/mdx/components"; The npm ecosystem is evolving, and if you're publishing TypeScript or MCP TypeScript SDKs, there are important changes on the horizon that you need to know about. ## The Landscape is Changing In the wake of recent supply chain attacks targeting the NPM ecosystem, the maintainers have made a decisive move to strengthen security across the platform. At the heart of these changes is a recognition that long-lived authentication tokens—once a convenient standard—have become a significant security vulnerability. Starting in October, NPM introduced a fundamental shift: all newly created write-enabled granular access tokens now come with a default expiration of just seven days, with a maximum lifespan of 90 days. But that's just the beginning. In the coming weeks, NPM will take two additional steps: - Revoking all existing legacy classic tokens for npm publishers - Permanently disabling legacy classic token generation on npmjs.com If your publishing workflow relies on these tokens, the clock is ticking. ## A Better Way Forward: Trusted Publishing Rather than simply adapting to shorter-lived tokens and the rotation headaches they bring, there's a more elegant solution: **Trusted Publishing**. At Speakeasy, we strongly recommend migrating to [trusted publishing](https://docs.npmjs.com/trusted-publishers) for your publishing workflows. Here's why it's worth the effort: - **Simplified authentication** using OpenID Connect (OIDC)—no more juggling tokens - **Zero token rotation** required—ever - **Automatic provenance attestation** for enhanced supply chain security It's not just more secure; it's actually easier to maintain. ## What You Need to Do ### Step 1: Update Your GitHub Workflow Permissions First, ensure your GitHub workflows have the necessary permissions to generate OIDC tokens. Add the `id-token: write` permission to any workflows used for publishing NPM packages: ```yaml name: Publish permissions: checks: write contents: write pull-requests: write statuses: write id-token: write # Required for OpenID Connect (OIDC) ``` Not sure if you've got it right? Run `speakeasy configure publishing` in your local repository. This command will automatically add the required permission and provide instructions tailored to your specific SDK configuration. ### Step 2: Configure Trusted Publishing on NPM Head over to your package settings on [npmjs.com](https://npmjs.com) and [configure trusted publishing](https://docs.npmjs.com/trusted-publishers#configuring-trusted-publishing): 1. Select **GitHub Actions** as your _Trusted Publisher_ 2. Enter the GitHub **user** and **Repository Name** for your SDK 3. Identify your publishing workflow file (typically found in `.github/workflows/`): - For `pr` mode: usually `sdk_publish.yaml` - For `direct` mode: usually `sdk_generation.yaml` 4. Leave the **Environment** field blank 5. Keep the **"Don't require two-factor authentication"** option selected in the `Publishing access` section. ### Step 3: Test Your Configuration Before you celebrate, let's make sure everything works: 1. Navigate to your repository's **Actions** tab on GitHub 2. Run the **Generate** workflow with these settings: - ✅ Check the `Force generation of SDKs` box - Bump the SDK version using the optional `set a specific SDK version` field 3. Once published, visit your package on [npmjs.com](https://npmjs.com) Look for two things that confirm success: - A green checkmark ✓ indicating the package was published using OIDC - A **Provenance** badge at the end of the README ## The Bottom Line The NPM ecosystem is moving toward a more secure future, and trusted publishing is the path forward. While these changes might require a bit of setup work now, they'll save you time and headaches down the road—not to mention significantly improving your supply chain security posture. Don't wait until your legacy tokens are revoked. Make the transition to trusted publishing today, and publish with confidence. --- _Questions about setting up trusted publishing for your Speakeasy-generated SDKs? Check out our [documentation](https://docs.npmjs.com/trusted-publishers) or reach out to our support team._ # on-demand-publishing-pre-releases Source: https://speakeasy.com/blog/on-demand-publishing-pre-releases import { Callout, ReactPlayer, YouTube } from "@/lib/mdx/components"; You've just generated your SDK, and you're feeling good, but in the immortal words of Kobe Bryant, "Jobs not finished. SDK published? I don't think so." Until it's published, your SDK is NOT done. Fortunately, with our recent release it's easier than ever to not only generate, but also publish your SDKs "On Demand". ## Pre-Releases & On-Demand Publishing
You can now add any pre-release version identifiers that you want to tag your SDK with: `-alpha`, `-beta`, etc. As you update your SDK, we'll automatically version bump while retaining the pre-release tags. Then when you're ready, you can remove the tag and publish your new version. And now, all this can be done from the Speakeasy dashboard, just head to the publishing tab and re-publish your SDKs whenever you want. --- ## 🚢 Improvements and Bug Fixes 🐛 Based on the most recent CLI version: [**Speakeasy v1.309.1**](https://github.com/speakeasy-api/speakeasy/releases/tag/v1.309.1) ### The Platform 🚢 Feat: Turn on Github Sandboxes by default for typescript, go and python SDKs \ 🚢 Feat: Imroved default target SDK naming from the classname \ 🐛 Fix: Improved handling of discriminated union handling ### Terraform 🚢 Feat: `x-speakeasy-match` supports subkeys for Terraform ### Java 🚢 Feat: Enabled custom base package naming in `gen.yaml` 🚢 Feat: License included in build.gradle \ 🚢 Feat: Improved comment syntax \ 🐛 Fix: `ClientCredentialsHook` compiles when global security absent ### C# 🚢 Feat: Added tolerance for extra json fields in union deserialization \ 🐛 Fix: Resolved union handling # one-click-request-sharing Source: https://speakeasy.com/blog/one-click-request-sharing ## New Features - **One-Click Request Sharing** - When you're combing through API traffic to debug customer issues and you find something you want to flag to your team, don't worry about taking a screenshot or formatting a payload. You can now share the URL of the filtered traffic or specific API request to any teammate on the Speakeasy team account, so that they can easily open up, and replay or export the request. Also available in the Request Viewer react embed. [Permalink Sharing - Watch Video](https://www.loom.com/share/91813dca7b1d4531b9b640c4fb004327) - **API Quickstart** - Get your API set up with Speakeasy in a minute. Our revamped API set up process makes it easy to get started whether you have an OpenAPI schema or are starting from scratch. [API Quickstart - Watch Video](https://www.loom.com/share/8752648ec6804b3c968a6d0bf55bc1f7) ## Smaller Changes - **JSON Schema Support - Upload your API existing schema without any alterations. We now support .json API schemas in addition to .yaml. Come with your API as it is, and get started using Speakeasy.** - **\[Bug Fix\] Big Query Schema Gen - If you are self-hosting Speakeasy on GCP, you can now accurately generate an OpenAPI schema off of API traffic stored in BigQuery.** # open-enums Source: https://speakeasy.com/blog/open-enums Today we're announcing support for "open" enums in our Go, Python and TypeScript code generators. This feature will help you ship SDKs that continue to work as you evolve your API and without causing your users unnecessary dependency management churn. ## What even is that? An enum is considered "closed" when it specifies a strict set of members like this TypeScript enum: ```typescript enum Color { Red = "red", Green = "green", Blue = "blue", } const value: Color = Color.Red; // ^ This can only be one of the three declared colors ``` Other languages, like Python and Java, treat enums similarly by default for example. An "open" enum, on the other hand, is one where unrecognized values can be expressed alongside the known members. Some languages may not have a way to make enums open but we can find a way such as with [branded types][branded-types] in TypeScript: [branded-types]: https://egghead.io/blog/using-branded-types-in-typescript ```typescript declare const brand: unique symbol; type Unrecognized = string & { [brand]: "unrecognized" }; function unrecognized(value: string): Unrecognized { return value as Unrecognized; } enum Color { Red = "red", Green = "green", Blue = "blue", } type OpenColor = Color | Unrecognized; const stillWorks: OpenColor = Color.Blue; const value: OpenColor = unrecognized("pink"); const badValue: Color = value; // ~~~~~~~~ // Type 'Unrecognized' is not assignable to type 'Color'. (2322) ``` > We'll continue using TypeScript as the target language for the rest of this > post. ## The problem Enums in OpenAPI and JSONSchema have presented developers with a unique challenge when it comes to making updates to their API. Let's consider an example where we have an API to fetch a blog post from a CMS: ```yaml openapi: 3.1.0 info: title: Blog CMS API summary: The API for the content management system of our blog. version: 0.1.0 servers: - url: https://headless-cms.example.com/api paths: /posts/{slug}: get: tags: "Other" operationId: get parameters: - name: slug in: path required: true schema: type: string responses: "200": description: A blog post content: application/json: schema: $ref: "#/components/schemas/BlogPost" components: schemas: BlogPost: type: object required: [id, slug, title, readingTime, markdown, category] properties: id: type: string slug: type: string title: type: string readingTime: type: integer markdown: type: string category: type: string enum: - photography - lifestyle - sports ``` If we're building a TypeScript SDK with [Zod][zod] for runtime validation from this API description, the code for the `BlogPost` schema might look like this: [zod]: https://zod.dev/ ```typescript import { z } from "zod"; export enum Category { Photography = "photography", Lifestyle = "lifestyle", Sports = "sports", } export const BlogPost = z.object({ id: z.string(), slug: z.string(), title: z.string(), readingTime: z.number().int(), markdown: z.string(), category: z.nativeEnum(Category), }); export type BlogPost = z.output; // ^? type BlogPost = { id: string; slug: string; title: string; readingTime: number; markdown: string; category: Category; } ``` We're showing an example here with TypeScript enums but the problem we're solving is also present when using literal string unions like if we were to define `Category` as: ```typescript export const Category = z.enum(["photography", "lifestyle", "sports"]); export type Category = z.infer; // ^? type Category = "photography" | "lifestyle" | "sports" ``` > The Speakeasy TypeScript SDK generator lets you choose if you want to generate > TypeScript-native enums or literal unions. We ship version 1 of our SDK and users can interact with the API like so: ```typescript import { CMS } from "@acme/cms"; const cms = new CMS(); const post = await cms.posts.get({ slug: "my-first-post" }); console.log(post.category); // -> photography ``` Some time passes and we've decide to add a new "tech" category to our API. We update our API description as follows: ```diff components: schemas: BlogPost: type: object required: [id, slug, title, readingTime, markdown, category] properties: id: type: string slug: type: string title: type: string readingTime: type: integer markdown: type: string category: type: string enum: - photography - lifestyle - sports + - tech ``` Once we deploy this change to our servers, users on v1 start getting validation errors because the category `tech` is not recognised by the SDK. ``` Uncaught: [ { "received": "tech", "code": "invalid_enum_value", "options": [ "photography", "lifestyle", "sports" ], "path": [], "message": "Invalid enum value. Expected 'photography' | 'lifestyle' | 'sports', received 'tech'" } ] ``` This is not a novel problem and depending on the language and API description format you're using, enums are sometimes treated as "closed" by default which gives rise to this challenge with evolving APIs. There is a longstanding [GitHub issue][oapi-enums] in the OpenAPI community to address this problem. In a different world, [protobuf enums][proto-enum] have varied representations where certain languages translate them to open enums and others to closed. [oapi-enums]: https://github.com/OAI/OpenAPI-Specification/issues/1552 [proto-enum]: https://protobuf.dev/programming-guides/enum/ ## How we're solving it Previously, the Speakeasy generator treated enums as closed and emitted code appropriately in target languages. Starting from today, we're exposing an OpenAPI extension, `x-speakeasy-unknown-values`, to allow you to selectively mark certain enums in your API description as open. To get started, add the extension to your enums: ```diff components: schemas: BlogPost: type: object required: [id, slug, title, readingTime, markdown, category] properties: # ... other fields omitted for brevity ... category: type: string + x-speakeasy-unknown-values: allow enum: - photography - lifestyle - sports ``` SDKs generated after this change will now have a `Category` enum type that is open. For TypeScript, the code we generate is equivalent to the following snippet: ```typescript import * as z from "zod"; import { catchUnrecognizedEnum } from "../../types"; // ^ A utility to capture and brand unrecognized values export enum Category { Photography = "photography", Lifestyle = "lifestyle", Sports = "sports", } export const BlogPost = z.object({ id: z.string(), slug: z.string(), title: z.string(), readingTime: z.number().int(), markdown: z.string(), category: z.nativeEnum(Category).or(z.string().transform(catchUnrecognizedEnum)), }); export type BlogPost = z.output; // ^? type BlogPost = { id: string; slug: string; title: string; readingTime: number; markdown: string; category: Category; } type OpenCategory = z.output["category"]; // ^? type OpenCategory = Category | Unrecognized ``` In case you're interested, here's how `catchUnrecognizedEnum` works to create a branded type: ```typescript declare const __brand: unique symbol; export type Unrecognized = T & { [__brand]: "unrecognized" }; export function catchUnrecognizedEnum(value: T): Unrecognized { return value as Unrecognized; } ``` > Speakeasy TypeScript SDKs explicitly emit TypeScript types that are tied to > Zod schemas instead of being inferred from them. Continuing with our example above, when the new "tech" category is introduced, the following code continues to compile and work: ```typescript import { CMS } from "@acme/cms"; const cms = new CMS(); const post = await cms.posts.get({ slug: "my-first-post" }); // ^ Previously, this would throw a validation error console.log(post.category); // -> tech ``` Users of the SDK also get the editor support they're used to when working with enums. For instance, switch-blocks work great for branch logic over enums: ```typescript import { CMS } from "@acme/cms"; import { Unrecognized } from "@acme/cms/types"; // ... let icon: "📸" | "🎨" | "🏈" | "❓"; switch (post.category) { case "lifestyle": icon = "🎨"; break; case "photography": icon = "📸"; break; case "sports": icon = "🏈"; break; default: post.category satisfies Unrecognized; // ^ Helps assert that our switch cases above are exhaustive icon = "❓"; break; } ``` Great! It seems we've done a lot of work to get back to client code that continues work. The net outcome, however, is that we've made more room for our APIs to evolve without causing issues for users on older versions of our SDK. This is _in addition_ to retaining good type safety, backed by strict runtime validation, and great developer experience (editor auto-complete continues to work for open enums). ## Wrapping up The "open" enums feature, using the `x-speakeasy-unknown-values` extension, is available for Go, Python and TypeScript SDKs with support for additional language targets being added in the future. Check out our docs on [customizing enums][customizing-enums] to learn about this and other customization options. [customizing-enums]: /docs/customize-sdks/enums # open-source-pledge-2024 Source: https://speakeasy.com/blog/open-source-pledge-2024 import { Table } from "@/mdx/components"; For as long as Speakeasy has had money in a bank account, we've been contributing to open-source projects we depend on. It therefore feels like a natural evolution to announce that we've joined [Open Source Pledge](https://opensourcepledge.com/) to formalize our support of the maintainers and projects that make our work possible. ## What is the Open Source Pledge? For too long Open Source software has been taken for granted by the software industry. The Open Source Software Pledge, initiated by Sentry, is attempting to change that by encouraging companies to commit to financially support open-source maintainers. The minimum participation is $2,000 per full-time employed developer per year. You can read more about it [here](https://opensourcepledge.com/about/). ## Speakeasy's open source contributions We donate a total of $3,523 per month ($42,276 per year) to OSS projects which annualizes to $4,228/engineer. Here's a breakdown of our monthly sponsorships:
## Why we support open source Like any software company in 2024, our success stands on the shoulders of open-source giants. Many of the projects we sponsor have been crucial in developing our product, while others play a vital role in maintaining a healthy API development ecosystem. ## Thank you, OSS community To all the maintainers, contributors, and issue raisers behind these open-source projects: thank you. Your dedication and hard work have not only made our work possible but have also paved the way for countless other developers and companies to innovate and succeed. We hope others will join us in supporting open-source software to create a sustainable future for the tools and technologies that power innovation. # open-unions-typescript-type-theory Source: https://speakeasy.com/blog/open-unions-typescript-type-theory ## The problem: API evolution Consider a typical API that returns user-linked accounts: ```typescript type LinkedAccount = | { provider: "google"; email: string; googleId: string } | { provider: "github"; username: string; githubId: number } | { provider: "email"; email: string; verified: boolean }; ``` Looking at this union, you can probably guess what's coming. Next quarter, the team adds Apple Sign-In. Then Microsoft. Maybe SAML for enterprise customers. This union **will** grow. Now imagine you've deployed an SDK with this type baked in. When the server starts returning `{ provider: "apple", ... }`, your application receives a 200 response, but then crashes trying to parse it. ``` Error: Unknown provider "apple" in LinkedAccount ``` Getting a successful HTTP response and then erroring is confusing. The user's code might not even care about the provider. They might just be checking if the user is authenticated. But one unexpected value nukes the entire response. At Speakeasy, we’re obsessed with building the best clients, and forward compatibility is a core part of that work. TypeScript’s structural type system introduces subtle constraints that make designing for forward compatibility non-trivial. In this article, we examine six approaches to designing forward-compatible unions, discuss the tradeoffs of each, and explain the approach we ultimately chose. **A note on API versioning:** Proper [API versioning](/blog/building-apis-and-sdks-that-never-break) can sidestep this problem, but most backends aren't built that way. Forward-compatible clients are often the more practical solution. **TL;DR:** Add an explicit unknown variant with a sentinel discriminator value: ```typescript type LinkedAccount = | { provider: "google"; email: string; googleId: string } | { provider: "github"; username: string; githubId: number } | { provider: "email"; email: string; verified: boolean } | { provider: "UNKNOWN"; raw: unknown }; ``` This preserves type narrowing on known variants while gracefully capturing anything new the server sends. The `raw` field holds the original payload for logging or custom handling. ## Approach 1: the starting point This is what most SDKs and API clients do today. Here's a typical discriminated union: ```typescript type Pet = Dog | Cat; type Dog = { kind: "dog"; name: string; barkVolume: number }; type Cat = { kind: "cat"; name: string; napsPerDay: number }; ``` This looks like clean, type-safe code. TypeScript can narrow based on the `kind` discriminator: ```typescript function describe(pet: Pet): string { switch (pet.kind) { case "dog": return `${pet.name} barks at volume ${pet.barkVolume}`; case "cat": return `${pet.name} takes ${pet.napsPerDay} naps daily`; } } ``` The compiler knows exactly which properties exist in each branch. But this type safety is an illusion. ❌ **The problem:** When the server starts returning parrots, your code throws runtime errors. You had compile-time confidence that didn't hold at runtime. ## Approach 2: the naive solution The obvious fix is to add `unknown` to handle future cases: ```typescript type Pet = Dog | Cat | unknown; ``` This compiles. Try to use it: ```typescript function describe(pet: Pet): string { if (pet.kind === "dog") { // Error: 'pet' is of type 'unknown' } } ``` ❌ What happened? TypeScript uses structural typing, not nominal typing. Types aren't defined by their names. They're defined by the **set of values** they can hold. A union is the set-theoretic union of its members. Here's the key insight: `unknown` is the **top type** in TypeScript. It's the set of all possible values. When you union something with its superset, the superset wins: ``` Dog | Cat | unknown = unknown ``` The union collapses. Your carefully crafted discriminated union becomes useless. (Note: `any` is also a top type with the [same absorption behavior](https://www.totaltypescript.com/an-unknown-cant-always-fix-an-any), but less safe.) This is set theory being unforgiving. You can't have a "weaker" type that represents "none of the above" because if it accepts more values, it's a superset, and supersets absorb their subsets. ## Approach 3: the monadic wrapper A pattern from functional programming, popularized by Rust's Result type, is to wrap values in a result type: ```typescript type Result = { ok: true; value: T } | { ok: false; raw: unknown }; type Pet = Result; ``` Now you can handle unknown cases: ```typescript function describe(pet: Pet): string { if (!pet.ok) { return `Unknown pet: ${JSON.stringify(pet.raw)}`; } switch (pet.value.kind) { case "dog": return `${pet.value.name} barks at volume ${pet.value.barkVolume}`; case "cat": return `${pet.value.name} takes ${pet.value.napsPerDay} naps daily`; } } ``` = { ok: true; value: T } | { ok: false; raw: unknown }; type Pet = Result; function describe(pet: Pet): string { if (!pet.ok) { return \`Unknown pet: \${JSON.stringify(pet.raw)}\`; } switch (pet.value.kind) { case "dog": return \`\${pet.value.name} barks at volume \${pet.value.barkVolume}\`; case "cat": return \`\${pet.value.name} takes \${pet.value.napsPerDay} naps daily\`; } }`} /> This works. Type narrowing is preserved, unknown cases are handled gracefully. ❌ **The problem:** Consider a realistic domain model where unions appear at multiple levels: ```typescript type Pet = Result; type Dog = { kind: "dog"; name: string; owner: Result }; type Cat = { kind: "cat"; name: string; owner: Result }; type Person = { type: "person"; name: string; address: Result }; type Company = { type: "company"; name: string; address: Result }; type Home = { location: "home"; city: string }; type Office = { location: "office"; city: string }; ``` Every level requires navigating through `.value`: ```typescript function getOwnerCity(pet: Pet): string | undefined { if (!pet.ok) return undefined; if (!pet.value.owner.ok) return undefined; if (!pet.value.owner.value.address.ok) return undefined; return pet.value.owner.value.address.value.city; } ``` Compare to what you **want** to write: ```typescript function getOwnerCity(pet: Pet): string | undefined { return pet.owner.address.city; } ``` The nesting exposes SDK internals when developers just want to think about their domain. They're thinking about pets and owners, not about whether the SDK successfully parsed a field. ❌ Another issue: switching a union from closed to open (or vice versa) becomes a breaking change. Adding the `Result` wrapper changes the type signature, forcing all consumers to update their code. That said, this pattern works well for unions without a common discriminator, like `string | { data: object }`. When there's no property to narrow on, wrapping is the only type-safe option. ## Approach 4: discriminator with string fallback We can do better than approach 3 if we have a common discriminator. The discriminator allows indexing into the union for TypeScript consumption: ```typescript type Pet = Dog | Cat | { kind: string; raw: unknown }; type Dog = { kind: "dog"; name: string; barkVolume: number }; type Cat = { kind: "cat"; name: string; napsPerDay: number }; ``` The idea: `kind: string` can hold any value beyond `"dog"` or `"cat"`, capturing unknown variants from the server. ❌ **The problem:** Narrowing breaks. When you check `pet.kind === "dog"`, TypeScript can't narrow to just `Dog`: ```typescript function describe(pet: Pet): string { if (pet.kind === "dog") { return pet.barkVolume.toString(); // Error: Property 'barkVolume' does not exist on type // 'Dog | { kind: string; raw: unknown; }' } return "other"; } ``` Since `"dog"` is a valid `string`, the `{ kind: string; raw: unknown }` variant still matches after the check. TypeScript can't eliminate it from the union. This is the same structural typing problem from approach 2: `string` is a superset of `"dog"`, so it absorbs the literal. ## Approach 5: discriminator with symbol What if we use a symbol to guarantee no collision? ```typescript const UNKNOWN_KIND: unique symbol = Symbol("UNKNOWN_KIND"); type UnknownPet = { kind: typeof UNKNOWN_KIND; raw: unknown }; type Pet = Dog | Cat | UnknownPet; // Narrowing works correctly function describe(pet: Pet): string { switch (pet.kind) { case "dog": return `${pet.name} barks at volume ${pet.barkVolume}`; case "cat": return `${pet.name} takes ${pet.napsPerDay} naps daily`; case UNKNOWN_KIND: return `Unknown pet: ${JSON.stringify(pet.raw)}`; } } ``` This solves the assignability problem. Symbols are completely distinct from string literals. ❌ **The problem:** Symbols introduce friction: - **Comparison requires imports:** Checking for unknown variants forces you to import from the SDK: ```typescript import { UNKNOWN_KIND } from "pets-sdk"; if (pet.kind === UNKNOWN_KIND) { // handle unknown } ``` - **Breaking changes:** If the union goes from closed to open, existing code that didn't account for symbols breaks: ```typescript function logPetKind(kind: string) { console.log(kind); } logPetKind(pet.kind); // Error: Argument of type 'string | typeof UNKNOWN_KIND' is not // assignable to parameter of type 'string'. ``` - **Serialization:** Symbols are stripped when serializing, e.g. using `JSON.stringify` ## Approach 6: the UNKNOWN literal string sentinel The solution is simpler than the previous approaches: ```typescript type Pet = Dog | Cat | { kind: "UNKNOWN"; raw: unknown }; type Dog = { kind: "dog"; name: string; barkVolume: number }; type Cat = { kind: "cat"; name: string; napsPerDay: number }; ``` Use an uppercase `"UNKNOWN"` string literal as the sentinel value. ```typescript function describe(pet: Pet): string { switch (pet.kind) { case "dog": return `${pet.name} barks at volume ${pet.barkVolume}`; case "cat": return `${pet.name} takes ${pet.napsPerDay} naps daily`; case "UNKNOWN": return `Unknown pet: ${JSON.stringify(pet.raw)}`; } } ``` Why this works: - `"UNKNOWN"` is a distinct literal, not a supertype of other values - Assignability is preserved: `Dog` clearly doesn't match `{ kind: "UNKNOWN" }` - No imports needed for comparison - Works with object keys and serialization ❌ **The problem:** What if the API later documents `kind: "UNKNOWN"` as a legitimate value? You'd need to rename the sentinel to `"_UNKNOWN_"` or similar, which is a breaking change for SDK consumers. We detect this during code generation and adjust accordingly, but it's something to be aware of. ## What we use in Speakeasy TypeScript SDKs In the Speakeasy SDK generator: - **When we can infer a discriminator:** We use approach 6 (the `"UNKNOWN"` sentinel). We have a smart algorithm for inferring discriminators in `oneOf`/`anyOf` schemas. It finds a property that's present on all variants with distinct literal values. - **When there's no common discriminator:** We fall back to approach 3 (the monadic wrapper). It's not as ergonomic, but it's the only type-safe option for polymorphic unions like `string | { data: object }`. The discriminator inference looks for: ```typescript // Detected: `a` is the discriminator { a: "x" } | { a: "y" } // Not detected: no single property distinguishes all variants { a: "x"; b: 1 } | { a: "x"; c: 1 } | { b: 1; c: 1 } ``` --- There are no perfect solutions here, only tradeoffs. Pick the approach that fits your constraints. _If you're building SDKs and want forward-compatible unions out of the box, [check out Speakeasy](https://www.speakeasy.com)._ # openapi-editor-comparison Source: https://speakeasy.com/blog/openapi-editor-comparison You're designing a new API endpoint. You need to generate the OpenAPI document, validate it against the required standards, collaborate with your team on the design, and keep it synchronized with your codebase as the API evolves. In this guide, we'll compare three popular OpenAPI editors: Speakeasy OpenAPI Editor, [Swagger Editor](https://swagger.io/tools/swagger-editor/), and [Postman](https://learning.postman.com/docs/design-apis/specifications/edit-a-specification/). We'll evaluate them based on what matters most when maintaining API specifications: editing experience, validation capabilities, workflow integration, and their fit within your broader API toolchain. ## OpenAPI document editing experience All three editors provide a familiar VSCode-like interface for authoring OpenAPI documents. The main difference lies in the depth of feedback and error detection they offer. ### Validation and error detection All three editors offer real-time validation, but the quality and depth of feedback vary based on the level of detail and help provided to address that feedback. Speakeasy provides the most comprehensive validation system, featuring three levels of feedback: - **Errors**: Specification violations that must be fixed. ![Errors](/assets/blog/openapi-editor-comparison/speakeasy-errors.png) - **Warnings**: Potential issues or best practices you're breaking that won't cause errors but may cause problems downstream. ![Warnings](/assets/blog/openapi-editor-comparison/speakeasy-warnings.png) - **Hints**: Suggestions for improving specification quality, like adding examples to parameters. For example, Speakeasy's hints might suggest: - Adding example values to request parameters. - Including response descriptions for all status codes. - Defining reusable components for repeated schemas. These suggestions enhance specification quality even when the document is technically valid. ![Hints](/assets/blog/openapi-editor-comparison/speakeasy-hints.png) This tiered approach helps you understand not just what's broken but what could be better. Speakeasy also includes a dedicated panel that lists all warnings, hints, and errors for easy navigation. ![Issues list](/assets/blog/openapi-editor-comparison/speakeasy-issues-panel.png) For production APIs, this validation depth matters. While live previews show what the API documentation will look like, comprehensive validation catches issues that could break your SDK generation, cause integration problems, or create confusion for API consumers. It's the difference between "Does this look right?" and "Is this OpenAPI document actually correct and complete?". Swagger Editor shows detailed error messages when you violate the OpenAPI specification but lacks the nuanced warnings or hints that help improve spec quality beyond basic correctness. ![Swagger UI path](/assets/blog/openapi-editor-comparison/swagger-validation.png) Postman also displays detailed error information when validation fails, focusing on specification compliance. ![Postman validation](/assets/blog/openapi-editor-comparison/postman-validation.png) ### Live preview Both Swagger Editor and Postman offer live preview capabilities. Swagger displays a full Swagger UI rendering of your API documentation, offering a consumer-facing view of your spec. ![Swagger UI](/assets/blog/openapi-editor-comparison/swagger-preview.png) Postman provides a more developer-oriented preview that shows individual OpenAPI components as you edit them. For instance, when working on a path, you'll see that component's properties rendered in the preview pane. ![Postman](/assets/blog/openapi-editor-comparison/postman-preview.png) ### Autocomplete Currently, only Postman provides autocomplete functionality, helping you write OpenAPI documents faster by suggesting valid OpenAPI properties and values as you type. | Feature | Speakeasy Editor | Swagger Editor | Postman Editor | | -------------------- | ---------------- | --------------- | ------------------- | | Real-time validation | ✅ | ✅ | ✅ | | Error details | ✅ | ✅ | ✅ | | Warnings | ✅ | ❌ | ✅ | | Quality hints | ✅ | ❌ | ❌ | | Error/warning panel | ✅ | ❌ | ✅ | | Live preview | ❌ | ✅ (Swagger UI) | ✅ (Component view) | | Autocomplete | ❌ | ❌ | ✅ | ## Specification fidelity All three editors support OpenAPI 3.x specifications and handle complex schemas, components, and syntax without issues. However, they diverge in their treatment of custom extensions. ### Extension support OpenAPI specifications allow for custom extensions (properties prefixed with `x-`) that teams use to add vendor-specific functionality or metadata to their OpenAPI documents. Speakeasy fully understands and validates custom extensions, which means you get proper validation, autocomplete, and documentation for your custom `x-` properties, treating them as first-class citizens in your specification. ![Speakeasy extensions](/assets/blog/openapi-editor-comparison/speakeasy-extensions.png) Swagger Editor and Postman accept custom extensions without throwing errors, but they don't provide any special handling, validation, or tooling support for them. Instead, the extensions are passed through as unknown properties. | Feature | Speakeasy Editor | Swagger Editor | Postman Editor | | ------------------------ | ---------------- | ------------------ | ------------------ | | OpenAPI 3.x support | ✅ | ✅ | ✅ | | Complex schema handling | ✅ | ✅ | ✅ | | Custom extension support | ✅ | ⚠️ (No validation) | ⚠️ (No validation) | ## Workflow integration How an OpenAPI editor fits into your development workflow can make the difference between a tool that streamlines your process and one that creates friction. ### Version control Speakeasy is built around a Git-based version control model. Every change is automatically committed with a hash, giving you a complete file history that you can navigate. ![File history](/assets/blog/openapi-editor-comparison/speakeasy-file-history.png) Speakeasy's Git-based model means every edit creates a commit automatically, which allows you to: - Review the complete change history with commit hashes. - Roll back to any previous version. You can see exactly what changed and when, and publish your OpenAPI document either to the main branch or as a custom tag. This automatic versioning preserves your OpenAPI document's changes history without any manual effort. ![Publishing to main](/assets/blog/openapi-editor-comparison/speakeasy-publishing.png) The free Swagger Editor doesn't include version control, which means your work is stored in the browser cache and can be lost when you clear it. However, SwaggerHub (the commercial platform) provides versioning capabilities where you can manually register versions, though it doesn't automatically create commits like Speakeasy. ![SwaggerHub versioning](/assets/blog/openapi-editor-comparison/swaggerhub-versioning.png) Postman does not provide built-in versioning for your OpenAPI documents. Version tracking only occurs when you update the version field in your document and sync it with collections. ### CI/CD capabilities Speakeasy excels in CI/CD integration. Publishing a new version to the main branch or creating a custom tag can automatically trigger downstream processes, such as generating new SDKs, updating MCP servers, or refreshing Terraform providers. This tight integration keeps your entire API toolchain synchronized with your OpenAPI document changes. ![Custom tags](/assets/blog/openapi-editor-comparison/speakeasy-custom-tags.png) The free Swagger Editor lacks CI/CD integration. SwaggerHub provides webhooks that fire with each new version, allowing teams to write custom scripts to handle these webhooks and trigger downstream processes like SDK regeneration or documentation updates. Postman does not provide any workflow integration or automation capabilities. ### Export and portability Speakeasy allows you to download your entire workspace as a ZIP file, including the OpenAPI document, overlays, and workflow files that define how the Speakeasy CLI combines overlays with your base specification. Speakeasy's [overlay](https://www.speakeasy.com/docs/prep-openapi/overlays/create-overlays) support allows teams to modify OpenAPI documents without touching the source file. For example, you might add vendor-specific extensions or customize descriptions for different audiences while keeping the base specification unchanged. The workflow file defines how overlays combine with the base OpenAPI document. ![Showing output files](/assets/blog/openapi-editor-comparison/speakeasy-export-files.png) Swagger Editor supports exporting your OpenAPI document in either JSON or YAML format. Postman does not offer a dedicated export feature for OpenAPI specifications from the Specs editor. | Feature | Speakeasy Editor | Swagger Editor (Free) | Postman Editor | | ------------------------- | ------------------------ | --------------------- | -------------- | | Automatic version control | ✅ (Git-based) | ❌ (SwaggerHub only) | ❌ | | Commit history | ✅ | ❌ (SwaggerHub only) | ❌ | | Branch/tag publishing | ✅ | ❌ | ❌ | | CI/CD automation | ✅ (SDK/tool generation) | ❌ (SwaggerHub only) | ❌ | | Export capabilities | ✅ (Full workspace ZIP) | ✅ (JSON/YAML) | ❌ | | Overlay support | ✅ | ❌ | ❌ | ## Ecosystem and integration OpenAPI editors differ in how they integrate with SDK generation and API tooling. ### SDK generation Speakeasy and Swagger Editor both offer SDK generation from your OpenAPI specification. Speakeasy specializes in SDK generation as its core product. It [supports eight languages](https://www.speakeasy.com/docs/sdks/create-client-sdks) and generates production-ready SDKs with: - Type safety and navigable structure. - Comprehensive documentation. - OAuth2, retries, and pagination support. - Webhook handling. - Idiomatic code that feels natural to developers in each language. - Extensibility and batteries-included features. The Editor integrates directly with this generation pipeline, providing validation and hints specifically designed to improve SDK quality. Swagger Editor uses [swagger-codegen](https://github.com/swagger-api/swagger-codegen) under the hood, which presents some challenges: - **Some unresolved bugs**: [Over 3k open issues](https://github.com/swagger-api/swagger-codegen/issues) as of October 2025. - **Inconsistent API feature coverage**: Struggles with OAuth, union types, and pagination in moderately complex APIs. - **Non-idiomatic code**: Originally a Java-based project, the generated code often feels Java-like, even in languages like Python or TypeScript, which can be off-putting for developers. Postman does not offer SDK generation capabilities. ### MCP servers [MCP](/mcp) (Model Context Protocol) servers allow users to interact with your APIs through LLMs, which is an increasingly essential capability as AI integration becomes standard. Swagger Editor enables the generation of MCP servers with streamable transports, but you are responsible for hosting, running, and maintaining them. Distribution typically happens via ZIP files or GitHub repositories with manual installation instructions for users. Speakeasy generates both local and remote MCP servers from your OpenAPI document. Within the Speakeasy Editor, you can visualize the artifacts for the MCP server along with the accepted parameters for each tool. ![Tool parameters](/assets/blog/openapi-editor-comparison/speakeasy-mcp-tools.png) Combined with Speakeasy's CI/CD integration, you can automatically update these MCP servers whenever your OpenAPI document changes, no manual code updates required. Postman does not offer MCP server generation. ## Pricing and accessibility [Swagger Editor](https://editor.swagger.io/) is completely free and open source, while [Swagger Pro](https://swagger.io/api-hub/essentials-trial/) ($9/month) adds code generation and API versioning. For team collaboration and CI/CD integrations like webhooks, [API Hub](https://swagger.io/tools/swaggerhub/pricing/) offers plans starting at $29/month for teams and $49/month for enterprises. Speakeasy provides its OpenAPI Editor as part of the Starter plan, which includes unlimited APIs with one SDK target. Postman offers the Spec editor free of charge. ## Which tool to use? The right OpenAPI editor depends on your team's workflow and priorities: - If you are looking for superior validation to ensure that your OpenAPI document is of top-tier quality and to make the creation of OpenAPI documentation, SDKs, or MCP server generation easier, then choose Speakeasy. - If you want to preview your UI without relying on generating tools from your OpenAPI document, the Swagger Editor is sufficient. - If the UI preview of your API doc in Swagger UI isn't essential, then Postman is a good option. ## Final thoughts Swagger Editor and Postman are great tools that set the standard for OpenAPI editing. At Speakeasy, our goal is to go far beyond those standards with a platform that's better for designing, validating, and managing your APIs end to end. Try it for yourself and experience the difference. # openapi-reference-guide Source: https://speakeasy.com/blog/openapi-reference-guide [Speakeasy has released a comprehensive, Open Source reference to writing OpenAPI specifications.](/openapi/) The reference has AI-search built in to help you quickly answer any questions that you may have. { /** * !NOTE: * This has been commented out because we don't currently allow * for contributions to this documentation. * * If you are interested in contributing, the GitHub repo can be found * [here](https://github.com/speakeasy-api/openapi-reference-documentation). */ } ## Why is OpenAPI Important? Consistent API design is essential. Building an API that developers enjoy interacting with, turns a SaaS business into a platform. However great design is only useful if it's well-documented and consistently represented across every API surface area (docs, SDKs, etc.). That is where OpenAPI comes in. Trying to create and maintain all your surfaces manually will inevitably lead to frustration and inconsistencies. With OpenAPI, you get much greater visibility into your API, and you can unify all aspects of your errors, responses, and parameters, ensuring consistency. If you are building a RESTful API, OpenAPI should be the source of truth that automates the creation of all your public surfaces (docs, SDKs, etc.) ![OpenAPI Spec](https://media.dev.to/cdn-cgi/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fxpq5c4ylckm40252u96r.png) This documentation will help you understand the OpenAPI Specification. ## What is OpenAPI? When we refer to OpenAPI, we mean the OpenAPI Specification - a standardized document structure for describing HTTP APIs in a way that humans and computers can understand. OpenAPI files are written as JSON or YAML, describing your API using a standard vocabulary defined by the Specification - we'll call this JSON or YAML file an OpenAPI document. A valid OpenAPI document describes your RESTful API and serves as the instruction set for tooling that generates API documentation, SDKs, and more. We will refer to an app or tool that reads an OpenAPI document to perform an action as an OpenAPI tool. Speakeasy is one such tool. ## OpenAPI Document Basics Your OpenAPI document is composed of keywords (some required, some optional). Together, the document covers the key elements of your API: - What security is required to access it? - Which endpoints expose which resources? - How are those resources constructed? ```yaml openapi: 3.1.0 info: title: The Speakeasy Bar version: 1.0.0 servers: - url: https://speakeasy.bar description: The production server security: - apiKey: [] tags: - name: drinks description: Operations related to drinks paths: /drinks: get: tags: - drinks operationId: listDrinks summary: Get a list of drinks responses: "200": description: A list of drinks content: application/json: schema: type: array items: $ref: "#/components/schemas/Drink" components: schemas: Drink: type: object title: Drink properties: name: type: string price: type: number securitySchemes: apiKey: type: apiKey name: Authorization in: header ``` To break that down piece by piece: `openapi`: The version of the OpenAPI Specification that the document conforms to, should be one of the supported versions. ```yaml openapi: 3.1.0 ``` Note: Speakeasy tooling currently only supports OpenAPI Specification versions 3.0.x and 3.1.x. `info`: Contains information about the document including fields like `title`, `version`, and `description` that help to identify the purpose and owner of the document. ```yaml info: title: The Speakeasy Bar version: 1.0.0 ``` `servers`: Contains an optional list of servers the API is available on. If not provided, the default URL is assumed to be /, a path relative to where the OpenAPI document is hosted. ```yaml servers: - url: https://speakeasy.bar description: The production server ``` `security`: Contains an optional list of security requirements that apply to all operations in the API. If not provided, the default security requirements are assumed to be [], an empty array. ```yaml security: - apiKey: [] ``` `tags`: Contains an optional list of tags that are generally used to group or categorize a set of Operations. ```yaml tags: - name: drinks description: Operations related to drinks paths: /drinks: get: tags: - drinks operationId: listDrinks summary: Get a list of drinks ``` `paths`: Contains the paths and operations available within the API. ```yaml paths: /drinks: get: tags: - drinks operationId: listDrinks summary: Get a list of drinks responses: "200": description: A list of drinks content: application/json: schema: type: array items: $ref: "#/components/schemas/Drink" ``` `components`: Contains an optional list of reusable schemas that can be referenced from other parts of the document. This improves the readability and maintainability of the document by allowing common schemas to be defined once and reused in multiple places. ```yaml components: schemas: Drink: type: object title: Drink properties: name: type: string price: type: number securitySchemes: apiKey: type: apiKey name: Authorization in: header ``` ## Format and File Structure An OpenAPI document is a JSON or YAML file that contains either an entire API definition or a partial definition of an API and/or its components. All field names in the specification are case-sensitive unless otherwise specified. A document can be split into multiple files, and the files can be in different formats. For example, you can have a JSON file that contains the API definition and a YAML file that contains the components, or a collection of files that contain partial definitions of the API and its components. Generally, the main API definition file is called `openapi.json` or `openapi.yaml`, and the component files are called `components.json` or `components.yaml`, though this is not a requirement. Some common organizational patterns for OpenAPI documents are: - A single file that contains the entire API definition. - A main file that contains the API definition and a components file that contains the components. - This is normally achieved by using the $ref keyword to reference the components file from the main file. Click here for more information on references. - A collection of files that contain partial definitions of the API and its components. - Some tools support this pattern by allowing multiple files to be provided. Others, such as the Speakeasy Generator, require the individual files to be merged into a single file before being passed to the tool, which can be achieved using the Speakeasy CLI tool. Click here for more information on the Speakeasy CLI merge tool. ## How is this different from the official OpenAPI documentation? The goal of this documentation is to provide a practitioner's guide for developers interested in understanding the impact of OpenAPI design on their downstream API surfaces. This guide prioritizes approachability and practicality over technical completeness. We've structured the documentation according to the needs of OpenAPI users of any skill level. ## Which versions of the OpenAPI Specification does this documentation cover? There are several versions of the OpenAPI specification in circulation: 2.0 (also known as Swagger), 3.0, and 3.1. We recommend developers use OpenAPI version 3.1 for all projects. The advantage of using OpenAPI version 3.1 is that it is fully compatible with JSON Schema, which gives you access to a much larger ecosystem of tools and libraries. Speakeasy's documentation covers versions `3.0.x` and `3.1.x` of the OpenAPI specification. Where there is an important difference between the two versions, we call it out specifically, otherwise, the documentation will apply to both versions. # ... Source: https://speakeasy.com/blog/openapi-servers import { Callout } from "@/mdx/components"; Hi! These blog posts have been popular, so we've built an entire [OpenAPI Reference Guide](/openapi/) to answer any question you have. It includes detailed information on [**servers**](/openapi/servers). Happy Spec Writing! In this post, we'll take a detailed look at how the OpenAPI Specification (OAS) allows us to define servers in our OpenAPI documents (OADs), then at a few tips to help you provide the best developer experience for your users. This aspect of API design is often overlooked, but specifying servers in a flexible and robust way can ensure your users know exactly where to send API calls, allows users the flexibility to pick a specific server based on their preference or data-privacy requirements, and allows your developers to switch between development and production environments while testing without editing code. ## A Brief History of Base URLs in Swagger (OAS 2.0) If you have some experience with Swagger 2.0, the predecessor to OpenAPI 3.0, you may recall that specifying servers for your API was somewhat rigid. We're including this to show you how to convert your Swagger 2.0 base URL definition to OpenAPI 3.x later on. If you don't have a Swagger 2.0 document, you can safely skip this section. In Swagger 2.0, we could define three fields to tell API users where to send requests: `host`, `basePath`, and `schemes`. The `host` specified the domain name or IP, the `basePath` defined the base path for the API, and `schemes` determined the protocols (HTTP or HTTPS) as an array. Here's an example of the relevant Swagger 2.0 fields: ```yaml filename="swagger-2.0.yaml" swagger: "2.0" host: api.example.com basePath: /v1 schemes: - https - http ``` To construct the base URL for requests, the API user should select a scheme, then concatenate the strings to form the URL: `schemes[n] + host + basepath`. ![Screenshot of Swagger 2.0 editor showing the scheme selector as the only URL-related option](/assets/blog/openapi-servers/scheme.png) The screenshot above illustrates the scheme selector in the Swagger 2.0 editor. The scheme selector is the only option for building a base URL in Swagger 2.0. ## Servers in OAS 3.x - Controlled Flexibility In response to the need for more flexibility, OAS 3.0 introduces the optional `servers` schema. The `servers` schema allows API users to select a base URL from an array of servers, some of which can include variables - allowing for the construction of more complex base URLs. Each server in the `servers` array consists of an object with at least one field, `url`. Each server object can also contain an optional `description` field, as well as an optional array of `variables`. Let's look at three different server objects to illustrate the available options, and how they influence the Swagger UI server selector. ```yaml openapi: 3.1.0 servers: - url: https://speakeasy.bar ``` The first server object is the simplest. It contains only a `url` field, which is a string containing the base URL for the API: `https://speakeasy.bar`. ```yaml servers: - description: The staging server. url: https://staging.speakeasy.bar ``` The second server object contains a `url` field and a `description` field. The `description` field is a string that can be used to describe the server. This is useful for providing additional information about the server, such as its location or purpose. In our example, the `description` field is used to describe the server as the staging server. ```yaml openapi: 3.1.0 servers: - description: A per-organization and per-environment API. url: https://{organization}.{environment}.speakeasy.bar variables: environment: default: prod description: The environment name. Defaults to the production environment. enum: - prod - staging - dev organization: default: api description: The organization name. Defaults to a generic organization. ``` The third server object contains a `url` field, a `description` field, and a `variables` field. The `variables` field is an array of objects that define variables that can be used to construct the base URL. In this example, the URL is a template string that contains two variables: `organization` and `environment`. Each variable's name is defined by the object's key. The `default` field is a string that defines the default value for the variable. The `enum` field is an array of strings that define the possible values for the variable. The `description` field is a string that can be used to describe the variable. This is useful for providing additional information about the variable, such as its purpose. This field is not used by the OpenAPI documentation generator, but it can be used by other tools to provide additional information to API users. ## Steps to Adapt Swagger 2.0 URL Definitions to OpenAPI 3.x 1. Identify Swagger 2.0 URL Components: Extract the `host`, `basePath`, and `schemes` values from your Swagger 2.0 document. 2. Create the servers array: Initiate a `servers` array in your OpenAPI 3.x document. 3. Translate URL Components: For each scheme in your Swagger 2.0 document, create a `server` object in the OpenAPI 3.x `servers` array. Combine the `scheme`, `host`, and `basePath` to form the `url` field of each server object. For example, if your Swagger 2.0 document contains the following URL components: ```yaml filename="swagger-2.0.yaml" swagger: "2.0" host: api.example.com basePath: /v1 schemes: - https - http ``` You would create the following `servers` array in your OpenAPI 3.x document: ```yaml filename="openapi.yaml" openapi: 3.1.0 servers: - url: https://api.example.com/v1 - url: http://api.example.com/v1 ``` ## Best Practices for Defining Servers in OpenAPI When defining servers in an OpenAPI document, it's crucial to strike a balance between flexibility and clarity. The goal is to provide enough information for users to understand and use the API effectively while allowing for various deployment scenarios. Here are some best practices to keep in mind: 1. **Provide clear and concise descriptions:** When using the `description` field, ensure it clearly indicates the purpose or specific use case of each server. This is particularly helpful in multiserver setups where different servers serve different roles, such as development, staging, and production. 2. **Use variables judiciously:** Variables offer great flexibility but can also introduce complexity. Use them sparingly and only when necessary, such as when users have unique base URLs or need to select a specific environment. 3. **Document server variables thoroughly:** If you use variables in your server URLs, provide clear documentation on what each variable represents and the valid values (`enum`) it can take. This documentation is crucial for users who need to understand how to construct the URLs correctly. 4. **Consider including common environment URLs:** While variables offer flexibility, sometimes it's easier for users if you explicitly list common environments (like `production`, `staging`, `development`) as separate server entries. This approach reduces the need for users to understand and manipulate variables. 5. **Validate server URLs regularly:** Use automated tools to validate that the URLs in the `servers` section are up to date and operational. This check is particularly important for APIs that undergo frequent changes or have multiple deployment environments. 6. **Stay consistent across documents:** If you manage multiple OpenAPI documents, maintain consistency in how servers are defined across them. This consistency helps users who work with multiple APIs in your suite, making it easier to understand and switch between them. By following these best practices, you can ensure that your OpenAPI server definitions are both functional and user-friendly, enhancing the overall usability and accessibility of your API. ## Advanced Server Definitions: Using Variables Server variables offer significant flexibility and adaptability when defining servers. This approach is especially useful in scenarios where the base URL of an API might change based on different environments (like production, staging, or development) or other factors (like user-specific or regional settings). Here's how to leverage variables effectively in your OpenAPI server definitions. ### Understanding Variables in Server URLs 1. **Defining variables:** Variables in OpenAPI are defined within the `servers` array. Each server object can have a `variables` field, which is an object itself. This object contains keys representing variable names, each with its set of properties. 2. **Properties of variables:** The properties of a variable can include: - `default`: A mandatory field that specifies the default value of the variable. - `enum`: An optional array of possible values the variable can take. - `description`: An optional field to describe the variable's purpose or usage. 3. **Using variables in URLs:** Variables are used within the `url` field of a server object, enclosed in curly braces `{}`. For example, `https://{environment}.api.example.com`. ### Best Practices for Using Variables 1. **Name variables clearly:** Choose clear and descriptive names for your variables. Names like `environment`, `region`, or `version` can be more intuitive for users to understand and use. 2. **Use sensible defaults:** Always provide sensible default values that point to the most common or recommended server configuration. This approach ensures that the API can be used out of the box without requiring users to modify the server URL. 3. **Restrict values with enums:** When appropriate, use `enum` to restrict the values that a variable can take. This is particularly useful for environment variables where only specific values like `production`, `staging`, and `development` are valid. 4. **Descriptive documentation:** Each variable should be accompanied by a description that explains its purpose and how it should be used. This is crucial for users who are unfamiliar with your API or its configuration options. 5. **Test variable-driven URLs:** Ensure that all combinations of variables and their values lead to valid and accessible API endpoints. This testing is critical to avoid configuration errors that could make the API unusable. ### Example of a Variable-Driven Server Definition ```yaml filename="openapi.yaml" openapi: 3.1.0 servers: - url: https://{environment}.api.example.com/v1 description: API server - selectable environment variables: environment: default: production enum: - production - staging - development description: Deployment environment of the API ``` In this example, the `environment` variable allows users to switch between different deployment environments without the need for separate server entries for each one. Use variables in server definitions to enhance the flexibility and scalability of your API configurations, catering to a wide range of deployment scenarios and user needs. ## Servers at the Path or Operation Level While it's common to define servers at the global level in OpenAPI documents, there are cases where specifying servers at the path or operation level is essential. This approach allows for more granular control, enabling different parts of the API to interact with different servers. It's particularly useful in microservices architectures, where different services may reside on different servers, or when specific operations require a unique endpoint. ### Overriding Servers at Path or Operation Level 1. **Path-level servers:** You can specify servers for individual paths. This is useful when different paths in your API are hosted on different servers. For instance, if your API handles file uploads and general data requests separately, you might have different servers handling each. 2. **Operation-level servers:** Similarly, servers can be specified for individual operations (GET, POST, PUT, DELETE, etc.). This granularity is beneficial when a specific operation, like a data submission (POST request), needs to be directed to a distinct server, like a secure data processing server. ### Best Practices - **Clear documentation:** Clearly document why certain paths or operations use different servers. This clarity helps developers understand the API's architecture and its interaction with various servers. - **Consistency in structure:** If you have different servers for different paths or operations, maintain a consistent structure in how these are defined to avoid confusion. - **Fallback to global servers:** Ensure that there is always a fallback server defined at the global level. This fallback acts as a default in cases where a path or operation doesn't explicitly specify a server. ### Example of a Server Definition at the Path Level Consider an API where file uploads are handled by a dedicated server. Here's how you might define this at the path level: ```yaml paths: /upload: servers: - url: https://upload.api.example.com description: Dedicated server for file uploads post: summary: Upload a file # ... other operation details ... ``` In this example, any POST requests to the `/upload` path would be directed to the specified upload server, differentiating it from other operations in the API that use the default global server. By strategically using servers at the path or operation levels, you can effectively manage different aspects of your API's functionality, ensuring that each part interacts with the most appropriate server. ## Case Studies: Effective Server Definitions in OpenAPI We'll now look at a few examples of how different companies have used OpenAPI server definitions to enhance their APIs' usability and functionality. ### Stripe: Path-Level Servers for File Uploads Stripe's OpenAPI documentation offers an excellent example of using path-level servers. In the Stripe API, the endpoint for file uploads is directed to the `https://files.stripe.com/` server, optimized for handling large data transfers. This separation ensures that the file upload process does not interfere with the regular API traffic and provides a more efficient way to handle resource-intensive operations. The following excerpt from [Stripe's OpenAPI document](https://raw.githubusercontent.com/stripe/openapi/master/openapi/spec3.yaml) illustrates this setup: ```yaml filename="stripe.yaml" openapi: 3.0.0 paths: /v1/files: post: operationId: PostFiles servers: - url: "https://files.stripe.com" # ... other operation details ... servers: - url: "https://api.stripe.com" ``` This setup demonstrates a clear understanding of operational requirements and a commitment to providing a seamless user experience. Stripe's OpenAPI document could be further enhanced by providing a fallback global server in case the file server is unavailable and documenting the reason for using a separate server for file uploads. ### Datadog: Dynamic Server Selection with Variables Datadog's API, known for its scalability and flexibility, leverages variables in server definitions to allow users to select their preferred region. This feature is particularly useful for global services where latency and data residency are critical considerations. The following setup allows users to dynamically choose the region closest to them, enhancing the performance and reliability of the AP:. ```yaml filename="datadog.yaml" openapi: 3.0.0 servers: - url: https://{subdomain}.{site} variables: site: default: datadoghq.com description: The regional site for Datadog customers. enum: - datadoghq.com - us3.datadoghq.com - us5.datadoghq.com - ap1.datadoghq.com - datadoghq.eu - ddog-gov.com subdomain: default: api description: The subdomain where the API is deployed. - url: "{protocol}://{name}" variables: name: default: api.datadoghq.com description: Full site DNS name. protocol: default: https description: The protocol for accessing the API. - url: https://{subdomain}.{site} variables: site: default: datadoghq.com description: Any Datadog deployment. subdomain: default: api description: The subdomain where the API is deployed. ... ``` ## Conclusion: The Impact of Well-Defined Servers in API Usability The way servers are defined in OpenAPI documents can significantly impact the usability and effectiveness of an API. Well-defined servers provide clear, accessible endpoints for different operational needs, improve the developer experience, and foster trust in the API's reliability and efficiency. Key benefits include: - **Enhanced clarity:** Clear server definitions help developers understand where and how to interact with the API. - **Operational flexibility:** Using variables and specifying servers at different levels (global, path, and operation) allows for greater operational flexibility and efficiency. - **Improved performance:** Strategic server allocation, such as dedicated endpoints for resource-intensive tasks, optimizes API performance. - **Global scalability:** Variables that allow for regional server selection make the API more adaptable to global users, addressing concerns like latency and data residency. ## Additional Resources and Tools for OpenAPI Server Definition If you're interested in deepening your understanding of OpenAPI server definitions, we recommend the following resources and tools: 1. **OpenAPI Specification:** The official [OpenAPI Specification](https://spec.openapis.org/oas/v3.1.0#server-object) documentation provides comprehensive guidance on server definitions and other aspects of OpenAPI. 2. **Swagger Editor:** An online tool that helps visualize and [edit OpenAPI documents](https://editor-next.swagger.io/), making it easier to define and review server configurations. 3. **The OpenAPI Specification Explained: API Servers:** A detailed [guide to the OpenAPI Servers](https://learn.openapis.org/specification/servers.html) that explains the different aspects of server definitions and how to use them effectively. 4. **Speakeasy documentation:** Our documentation on how to [configure your servers](/docs/customize-sdks/servers) in OpenAPI while creating SDKs with Speakeasy. # openapi-spec-drift-detection Source: https://speakeasy.com/blog/openapi-spec-drift-detection ### New Features - **Know when your API drifts from your docs** - The Speakeasy SDK now checks API traffic against your uploaded OpenAPI schema. When traffic that is not present in your schema is served by your API, your team will be able to diff schemas and see where drift has occurred. - **Push Alerts to the whole team** \- To enable automation, Speakeasy pushes an event to your webhook for each unknown endpoint when drift is detected. Integrate your team's tooling with our webhooks to create automatic Slack alerts so you know as soon as something changes! ### Smaller Improvements - Speakeasy is now available for APIs written in **typescript**, thanks to our typescript SDK! # openapi-studio-laravel-integration-and-what-in-zod-s-name Source: https://speakeasy.com/blog/openapi-studio-laravel-integration-and-what-in-zod-s-name import { Callout, ReactPlayer } from "@/lib/mdx/components"; If you like scrolling through `yaml` & `json` documents, ignore this update. If you'd be quite happy to get some help editing your OpenAPI spec, then read on! And if [last week's PHP announcement](/changelog/changelog-2024-09-18) got you excited, then prepare yourself for more good things: Laravel integration support! ## AI-powered spec improvement with OpenAPI Studio
Speakeasy's new AI-powered OpenAPI Studio has arrived! Here's what you can expect: - **AI-suggested improvements**: Our AI will analyze your OpenAPI document and provide tailored suggestions to enhance it for optimal SDK creation. - **Local sync**: the OpenAPI Studio stays in perfect sync with changes made to your locally saved spec to maintain a single source of truth. - **Clean versioning with overlays**: All edits are saved in OpenAPI overlay files, keeping your root OpenAPI spec pristine while allowing for flexible, version-controlled improvements. This feature is designed to streamline your workflow, ensuring your APIs are always primed for top-notch SDK generation. ## Laravel-compatible packages for every API ```php use Dub\Dub; use Illuminate\Contracts\View\View; use Illuminate\Http\Request; final readonly class LinksController { public function __construct( private Dub $dub, ) {} public function index(Request $request): View { return view('links.index', [ 'links' => $this->dub->links->list(), ]); } } ``` Building on our recent [type-safe PHP generation release](/post/release-php), we're excited to introduce Laravel integration support. Now, when you generate PHP SDKs with Speakeasy, you can opt to create a Laravel-compatible package. What's included: - Automatic Service Provider Creation: We'll generate the necessary service provider for your package. - Configuration File Setup: Get config files tailored for the Laravel ecosystem. - Laravel Package Structure: Your SDK will be structured as a proper Laravel package, ready for distribution. It's never been easier to make your API native to the Laravel ecosystem! ## What in Zod's name? ![Zod.fyi screenshot](/assets/changelog/assets/zod-fyi.jpeg) [Zod](https://zod.dev/) powers runtime validation in our TypeScript generation. Depending on the complexity of a Zod schema, the resulting validation error message can contain a wall of JSON text - the serialised issues that were recorded during validation. We've created a small tool to help better visualize and parse the errors. It's built as a web UI which provides yo with sharable URLs for easy collaboration. We hope it will help the TypeScript community be able to more easily build with Zod! Try it out at [zod.fyi](https://zod.fyi)! --- ## 🐝 New features and bug fixes 🐛 Based on the most recent CLI version: [**Speakeasy v1.398.0**](https://github.com/speakeasy-api/speakeasy/releases/tag/v1.398.0) ### Generation platform 🐝 Feat: improvements to the error handling readme section \ 🐝 Feat: support path, header, and query parameter assertions in mock server \ 🐝 Feat: improved snapshot, and performance testing \ 🐛 Fix: address missing examples and improve number examples ### Python 🐛 Fix: patched implementation of optional responses \ 🐛 Fix: enable const support for Python 3.9.x environments ### TypeScript 🐛 Fix: pagination now works when parameter flattening is enabled ### PHP 🐝 Feat: Laravel integration support \ 🐝 Feat: improved PHP usage snippet generation # Here we are describing the Global security schemes used by the operations in this document Source: https://speakeasy.com/blog/openapi-tips-auth import { Callout } from "@/mdx/components"; Hi! These blog posts have been popular, so we've built an entire [OpenAPI Reference Guide](/openapi/) to answer any question you have. It includes detailed information on [**API authentication**](/openapi/security). Happy Spec Writing! ## The Problem The OpenAPI spec is best known for descriptions of RESTful APIs, but it's designed to be capable of describing any HTTP API whether that be REST or something more akin to RPC based calls. That leads to the spec having a lot of flexibility baked-in; there's a lot of ways to achieve the exact same result that are equally valid in the eyes of the spec. Because of this, [the OpenAPI](https://spec.openapis.org/oas/v3.1.0#operation-object) documentation is very ambiguous when it comes to how you should define your API. That's why we'd like to take the time to eliminate some of the most common ambiguities that you'll encounter when you build your OpenAPI spec. In this case we'll be taking a look at **how to correctly configure auth in your OpenAPI spec.** ## What Authentication mechanisms are available? OpenAPI supports a number of different options for API authentication, which can be daunting when first starting out. Before we give our thoughts on the different methods, it's worth highlighting that regardless of the method of authentication you choose, you should pair it with TLS. TLS encrypts the messages to and from your API, to protect you and your users from attack. [Learn more about setting up TLS here](https://letsencrypt.org/getting-started/). Some of the common types of authentication are listed below: - **apiKey**: This is the most common form of authentication for machine-to-machine (M2M) APIs and supports passing a pre-shared secret in a number of different ways i.e. either via the _Authorization_ header (or another custom header), as a query parameter, or via a cookie. While this is probably the most commonly used mechanism, it is generally one of the least secure. This is especially true if the key is passed outside of headers or cookies (i.e. via query params as various logging mechanisms normally store query param information). The biggest security flaw is that most pre-shared secrets are long lived and if intercepted can be used until they are either revoked or expire (generally in a number of months or years). This risk is normally tolerated for M2M applications as the chance of interception (especially when using private VPCs/TLS and other mechanisms) is relatively low when compared to a key from a user's device traveling on a public network. - **basic**: This is a simple authentication mechanism baked into the HTTP protocol. It supports sending an _Authorization_ header containing an encoded username and password. While this can be a relatively simple mechanism to get started with, if used incorrectly can risk leaking easy to decode passwords. It also shares a lot of the downsides of apiKeys below. - **bearer**: This scheme allows the passing of a token (most commonly a JWT) in the _Authorization_ header. This is generally used for short lived tokens that are granted to the users of your API through an additional login mechanism. Using a JWT allows for the storage of additional metadata within the token which can be helpful for some use cases, such as storing scopes for permissions models. - **oauth2**: A popular open authentication mechanism that supports an authentication flow that allows servers to authenticate on behalf of a user or organization. While more generally used for authenticating clients and end-users it is quite often used in machine-to-machine applications as well, but is less popular due to the added complexity of the authentication flows. OAuth2 is considered more secure than other mechanisms due to its granted privileges through short lived tokens, that limit damage from intercepting the tokens. - **openIdConnect**: Is an authentication mechanism built on top of OAuth2 that allows obtaining identity information about the authenticating user via JWTs. ## Global Authentication vs Endpoint Authentication The OpenAPI specification allows you to describe all the above authentication mechanisms and more from the [HTTP Authentication Scheme Registry](https://www.iana.org/assignments/http-authschemes/http-authschemes.xhtml). Describing security in your OpenAPI document is then done through 1 of 2 different options: - **Global security**: the security you describe is available for all operations in your document. - **Per operation security:** when described it overrides any global level security described. Here is an example of describing security in the ways mentioned above: ```yaml openapi: 3.0.3 info: title: Example Security Definitions version: 1.0.0 servers: - url: http://api.prod.speakeasy.com # This is a list of security schemes names defined in the components section security: - APIKey: [] components: # The definition of the used security schemes securitySchemes: APIKey: type: apiKey in: header name: X-API-Key Bearer: type: http scheme: bearer bearerFormat: JWT paths: /test: get: # The security schemes defined here will override the global security schemes for this operation security: - Bearer: [] responses: 200: description: OK # This operation used the global security schemes defined as it doesn't provide its own delete: responses: 200: description: OK ``` The important parts of the above example are the [security](https://spec.openapis.org/oas/v3.1.0#security-requirement-object) and [securitySchemes](https://spec.openapis.org/oas/v3.1.0#security-scheme-object/security-schemes) sections. We will go into some details about how they are defined and the options available. ## How To Describe Security The [security](https://spec.openapis.org/oas/v3.1.0#security-requirement-object) section is a list (actually a list of key-value pairs, but we will talk a bit more about that later) of security schemes that can be used to authenticate all operations or a particular operation (depending on the scope of the [security](https://spec.openapis.org/oas/v3.1.0#security-requirement-object) list). Below is an example of a number of different ways you can use the [security](https://spec.openapis.org/oas/v3.1.0#security-requirement-object) section of your document: ```yaml # The below example shows a single mandatory scheme needed for the API security: - APIKey: [] # The below example shows that one of the below schemes is required for the API security: - APIKey: [] - Bearer: [] # The below example shows there is no security required for the API # this is equivalent to not having a security section at all at the Global scope # or disabling security at the per operation level security: [] # The below example shows that security is optional for the API # this may be used if an API provides additional functionality when authenticated security: - APIKey: [] - {} # The below example shows that certain scopes are required by the OAuth token used # to authenticate the API security: - OAuth: - read - write ``` The items in the list are key-value pairs with a name or key of a security scheme defined in the components section. We recommend giving them a boring name that explains what they are. The values are an array of scopes used only by the [oauth2](https://spec.openapis.org/oas/v3.1.0#oauth2-security-requirement) and [openIdConnect](https://tools.ietf.org/html/draft-ietf-oauth-discovery-06) type schemes, and define what scopes are needed for the API. When used as shown above it provides a list of available schemes that can be used, with the end-user of the API being able to choose one of the available schemes to use to authenticate. If more than one scheme is required to authenticate an API, then that is where additional pairs in the key-value pairs come in. See the example below: ```yaml # The below example shows 2 options for an end user to choose, as long as they use one or the other # they will be able to access the API security: - APIKey: [] - Bearer: [] # The example below differs as it is a single option with multiple schemes # Both the APIKey and SigningKey need to be used together to access the API security: - APIKey: [] SigningKey: [] # The example below shows multiple options for an end user to chose # with one of them requiring the use of multiple schemes security: - APIKey: [] SigningKey: [] - Bearer: [] ``` Combining schemes like above give you the option to define AND/OR type functionality when it comes to the requirements of your API. ## How To Describe Security Schemes [securitySchemes](https://spec.openapis.org/oas/v3.1.0#security-scheme-object/security-schemes) are the actual details of the options provided in the [security](https://spec.openapis.org/oas/v3.1.0#security-requirement-object) sections of your document. The security schemes are components that are defined with the [components](https://spec.openapis.org/oas/v3.1.0#components-object) section of your document. Below is an example of the 5 types of security schemes described above and how they are defined: ```yaml --- components: schemas: ... responses: ... # The definition of the used security schemes securitySchemes: BasicAuth: # An arbitrary scheme name, we recommend something descriptive type: http scheme: basic Bearer: type: http scheme: bearer bearerFormat: JWT # Optional token format APIKey: type: apiKey in: header # or query/cookie name: X-API-Key OAuth: type: oauth2 flows: # Many different flows are available - https://spec.openapis.org/oas/v3.1.0#oauth-flows-object implicit: authorizationUrl: https://test.com/oauth/authorize scopes: read: Grants read access write: Grants write access OpenIdConnect: type: openIdConnect openIdConnectUrl: https://test.com/.well-known/openid-configuration ``` ## Best Practices I generally recommend considering developer experience and weighing this up against the security requirements of your API. Consider its use cases such as will it be called from another server? A client? Or a combination of both. Based on your needs then try to describe your security requirements in your OpenAPI document as simply as possible, if you can avoid multiple options or too many per operation differences then it will generally require less friction for your end-user to get up and running and start using your API. This is the main reason we still see pre-shared secrets (described by the [apiKey](https://spec.openapis.org/oas/v3.1.0#api-key-sample) type above) being the most ubiquitous option amongst APIs today, but if not managed correctly it can be one of the least secure options available. # A basic string Source: https://speakeasy.com/blog/openapi-tips-data-type-formats import { Callout, Table } from "@/mdx/components"; Hi! These blog posts have been popular, so we've built an entire [OpenAPI Reference Guide](/openapi/) to answer any questions you have. It includes detailed information on [**data types**](/openapi/schemas). Happy Spec Writing! ## The problem The OpenAPI spec is best known for descriptions of RESTful APIs, but it's designed to be capable of describing any HTTP API whether that be REST or something more akin to RPC based calls. That leads to the spec having a lot of flexibility baked-in: there's a lot of ways to achieve the exact same result that are equally valid in the eyes of the spec. Because of this, [the OpenAPI](https://spec.openapis.org/oas/v3.1.0#operation-object) documentation is very ambiguous when it comes to how you should define your API. That's why we're taking the time to eliminate some of the most common ambiguities that you'll encounter when you build your OpenAPI spec. In this case we'll be taking a look at **how to effectively use data types in your OpenAPI 3.0.X spec.** Note: We will cover the differences introduced by 3.1 in a future post. ## Recommended practices The OpenAPI Spec gives you plenty of options for describing your types, but also a lot of options to describe them loosely. Loose is fine if your goal is to have a spec that is valid, but if you are using your OpenAPI document to generate: code, documentation or other artifacts, loose will get you into trouble. **Describe your types as accurately as possible**; you will not only improve the documentation of your API reducing ambiguity for end-users), but **will give as much information as possible to any tools you might be using to generate code, documentation or other artifacts from your OpenAPI document.** Concretely, we recommend that you: - Describe your types as explicitly as possible by using the OpenAPI defined formats. - Use additional validation attributes as much as possible: mark properties as required, set readOnly/writeOnly, and indicate when fields that are nullable. Below, we will step through the different types available in OpenAPI and explain how to use formats, patterns and additional attributes to give you a spec that is descriptive and explicit. ## The data types In addition to an **object** type, for custom type definitions, the [OpenAPI Specification](https://spec.openapis.org/oas/latest.html#data-types) supports most of the “primitive” types and objects you would expect to describe what your API is capable of sending and receiving: - [**string**](/openapi/schemas/strings) - [**number**](/openapi/schemas/numbers) - [**integer**](/openapi/schemas/numbers) - [**boolean**](/openapi/schemas/booleans) - [**array**](/openapi/schemas/arrays) For each of these primitive types, there is a set of commonly-used **formats** (i.e. date format for string) which you can designate to enforce additional constraints on the values of a schema or field. There is also the option of associating a **nullable** attribute. These options lead to a number of different possibilities for describing your data. The OpenAPI Spec also includes the ability to describe more complex relationships between types using the **oneOf/anyOf/allOf** attributes and providing the ability to describe **enums** but we will leave the discussion of them to a future blog post. For now, let's explore the various types and options available for describing your types. ### string Of the primitive types (ignoring the **object** type) , the **string** type is the most flexible type available. In addition to being able to be used to represent other types (such as `“true”`, `“100”`, `“{\\“some\\”: \\”object\\”}”`), it supports a number of formats that overlay constraints to the type of data represented. This is useful for mapping to types in various languages if you are using the OpenAPI spec for code generation. #### Formats The string type via the OpenAPI Specification officially supports the below formats:
The **format** attribute can also be used to describe a number of other formats the string might represent but outside the official list above, those formats might not be supported by tooling that works with the OpenAPI Spec, meaning that they would be provided more as hints to end-users of the API: - email - uuid - uri - hostname - ipv4 & ipv6 - and others Below are some examples of describing various string types: ```yaml schema: type: string # A string that represents a RFC3339 formatted date-time string schema: type: string format: date-time # A string that represents a enum with the specified values schema: type: string enum: - "one" - "two" - "three" # A string that represents a file schema: type: string format: binary ``` #### Patterns The **string** type also has an associated **pattern** attribute that can be provided to define a regular expression that should be matched by any string represented by that type. **The format of the regular expression is based on** [**Javascript**](https://262.ecma-international.org/5.1/#sec-15.10.1) and therefore could describe regular expressions that might not be supported by various tools or target languages, so **make sure to check the compatibility with your intended targets**. Example of a string defined with a regex pattern: ```yaml # A string that must match the specified pattern schema: type: string pattern: ^[a-zA-Z0-9_]*$ ``` ### number/integer The **number/integer** types allow the description of various number formats through a combination of the **type** and **format** attributes, along with a number of attributes for validating the data, the spec should cover most use cases. Available formats are:
Below are some examples of defining **number/integer** types: ```yaml # Any number schema: type: number # A 32-bit floating point number schema: type: number format: float # A 64-bit floating point number schema: type: number format: double # Any integer schema: type: integer # A 32-bit integer schema: type: integer format: int32 # A 64-bit integer schema: type: integer format: int64 ``` Various tools may treat a **number/integer** without a format attribute as a type capable of holding the closest representation of that number in the target language. For example, a **number** might be represented by a **double,** and an **integer** by an **int64.** Therefore, it's recommended that you **be explicit with the format of your number type and always populate the format attribute**. The **number** type also has some optional attributes for additional validation: - **minimum**: The **minimum** inclusive number the value should contain. - **maximum**: The **maximum** inclusive number the value should contain. - **exclusiveMinimum**: Make the **minimum** number exclusive. - **exclusiveMaximum**: Make the **maximum** number exclusive. - **multipleOf**: Specify the **number/integer** is a multiple of the provided value. Some examples are below: ```yaml # An integer with a minimum inclusive value of 0 schema: type: integer format: int32 minimum: 10 # An integer with a minimum exclusive value of 0 schema: type: integer format: int32 minimum: 0 exclusiveMinimum: true # A float with a range between 0 and 1 schema: type: number format: float minimum: 0 maximum: 1 # A double with an exclusive maximum of 100 schema: type: number format: double maximum: 100 exclusiveMaximum: true # An 64 but integer that must be a multiple of 5 schema: type: integer format: int64 multipleOf: 5 ``` ### boolean The boolean type is simple; it represents either **true** or **false**. Be aware that it doesn't support other truthy/falsy values like: **1** or **0**, an empty string “” or **null**. It has no additional attributes to control its format or validation. ```yaml # A boolean type schema: type: boolean ``` ### array The **array** type provides a way of defining a list of other types through providing an **items** attribute that represents the schema of the type contained in the array. ```yaml # An array of string schema: type: array items: type: string # An array of objects schema: type: array items: type: object properties: name: type: string age: type: integer # An array of arbitrary things schema: type: array items: {} ``` The **array** type will support any schema that describes any other type in its items attribute including types using **oneOf/anyOf/allOf** attributes. The **array** type also has some optional attributes for additional validation: - **minItems**: The minimum number of items the array must contain. - **maxItems**: The maximum number of items the array must contain. - **uniqueItems**: The array must contain only unique items. ```yaml # An array of floats that must contain at least 1 element. schema: type: array items: type: number format: float minItems: 1 # An array of strings that must contain at most 10 elements. schema: type: array items: type: string maxItems: 10 # An array of booleans that must contain exactly 3 elements. schema: type: array items: type: boolean minItems: 3 maxItems: 3 # An array of strings that must contain only unique elements. schema: type: array items: type: string uniqueItems: true ``` ### object The **object** type allows simple and complex objects, dictionaries, and free-form objects, along with a number of attributes to control validation. #### Fully typed object Fully typed objects can be described by providing a properties attribute that lists each property of the object and its associated type. ```yaml # A fully typed object schema: type: object properties: name: type: string age: type: integer format: int32 active: type: boolean # A fully typed object with a nested object schema: type: object properties: name: type: string age: type: integer format: int32 active: type: boolean address: type: object properties: street: type: string city: type: string state: type: string zip: type: string ``` Objects with properties have access to some additional attributes that allow the objects to be validated in various ways: - **required**: A list of properties that are required. Specified at the object level. - **readOnly**: A property that is only available in a response. - **writeOnly**: A property that is only available in a request. ```yaml # A fully typed object with all fields required schema: type: object properties: name: type: string age: type: integer format: int32 active: type: boolean required: - name - age - active # A fully typed object with only one field required schema: type: object properties: name: type: string age: type: integer format: int32 active: type: boolean required: - name # A fully typed object with some field as read-only schema: type: object properties: name: type: string age: type: integer format: int32 active: type: boolean readOnly: true # This field is only returned in a response required: - name - age - active # This field will only be required in a response # A fully typed object with some field as write-only schema: type: object properties: name: type: string age: type: integer format: int32 active: type: boolean isHuman: type: boolean writeOnly: true # This field is only required in a request required: - name - age - active - isHuman # This field will only be required in a request ``` #### Using object for dictionaries The **object** type can also be used to describe dictionaries/maps/etc that use strings for keys and support any value type that can be described by the OpenAPI Spec. ```yaml # A dictionary of string values schema: type: object additionalProperties: type: string # A dictionary of objects schema: type: object additionalProperties: type: object properties: name: type: string age: type: integer format: int32 ``` You can also describe dictionaries that will contain certain keys ```yaml # A dictionary that must contain at least the specified keys schema: type: object properties: name: type: string # Must match type of additionalProperties required: - name additionalProperties: type: string ``` When using the **additionalProperties** attribute you can also specify additional attributes to validate the number of properties in the object: - **minProperties**: The minimum number of properties allowed in the object. - **maxProperties**: The maximum number of properties allowed in the object. For example: ```yaml # A dictionary of string values that has at least one key. schema: type: object additionalProperties: type: string minProperties: 1 # A dictionary of string values that has at most 10 keys. schema: type: object additionalProperties: type: string maxProperties: 10 # A dictionary of string values that has 1 key. schema: type: object additionalProperties: type: string minProperties: 1 maxProperties: 1 ``` #### Free-form objects The **object** type can also be used to describe any arbitrary key/value pair (where the keys are still required to be strings). ```yaml # An arbitrary object/dictionary that can contain any value. schema: type: object additionalProperties: true # An alternate way to specify an arbitrary object/dictionary that can contain any value. schema: type: object additionalProperties: {} ``` ### null OpenAPI 3.0.X doesn't support a null type but instead allows you to mark a schema as being nullable. This allows that type to either contain a valid value or null. ```yaml # A nullable string schema: type: string nullable: true # A nullable integer schema: type: integer format: int32 nullable: true # A nullable boolean schema: type: boolean nullable: true # A nullable array schema: type: array items: type: string nullable: true # A nullable object schema: type: object properties: foo: type: string nullable: true ``` # openapi-tips-oneof-allof-anyof Source: https://speakeasy.com/blog/openapi-tips-oneof-allof-anyof import { Callout } from "@/mdx/components"; Hi! These blog posts have been popular, so we've built an entire [OpenAPI Reference Guide](/openapi/) to answer any question you have. It includes detailed information on [**polymorphic types**](/openapi/schemas/objects/polymorphism). Happy Spec Writing! The OpenAPI Specification (OAS) is designed to be capable of describing any HTTP API, whether that be REST or something more akin to RPC-based calls. That leads to the OAS having a lot of flexibility baked-in: there are a lot of ways to achieve the exact same result that are equally valid in the eyes of the OAS. That's why we're taking the time to eliminate some of the most common ambiguities that you'll encounter when you build your OpenAPI documents (OADs). In this case, we'll be taking a look at **how to effectively use anyOf, allOf, and oneOf in your OpenAPI 3.X OADs.** The `anyOf`, `allOf`, and `oneOf` keywords are defined by JSON Schema and used in OpenAPI to define the structure and validation rules for data. They can be used together to define complex and flexible schemas. - **`oneOf`:** The value must match exactly one of the subschemas. The `oneOf` keyword is useful for describing scenarios where a property can be defined with multiple possible data structures, but only one of them is used at a time. For example, if your API accepts a `string` or an `int` for a certain field depending on the use case, `oneOf` would be used. In code generation, it will be generally interpreted as a union type. - **`allOf`:** The value must match all of the subschemas. The `allOf` keyword is useful for describing model composition: the creation of complex schemas via the composition of simpler schemas. - **`anyOf`:** The value must match one or more of the subschemas. The `anyOf` keyword is useful for describing type validation (similar to `oneOf`), but it can get you into a lot of trouble in code generation. There is no straightforward way for a code generator to interpret what `anyOf` means, which can lead to undefined or unintended behavior or simply any schema being allowed. We'll dig into this more later. ## **Recommended Practices** When you're writing your OAD, you need to consider your end goals. The distinctions between allOf, oneOf, anyOf are subtle, but the implications on types in a generated SDK can be huge. To avoid downstream problems, we recommend following these rules: - Use `oneOf` to represent union type object fields. - Use `allOf` to represent intersection type / composite objects and fields. - Don't use `anyOf` unless you absolutely need to. Below, we will step through each of the different keywords and explain how to use formats, patterns, and additional attributes to give you a spec that is descriptive and explicit. ## **What is `oneOf`?** The `oneOf` keyword in JSON Schema and OpenAPI specifies that a value must match **exactly one** from a given set of schemas. `oneOf` is the closest OpenAPI analog to the concept of a union type. A union type is a way to declare a variable or parameter that can hold values of multiple different types. They allow you to make your code more flexible while still providing type safety to users. Let's look at an example of how a `oneOf` is translated into a typescript object: ```yaml components: schemas: Drink: type: object oneOf: - $ref: "#/components/schemas/Cocktail" - $ref: "#/components/schemas/Mocktail" ``` That would produce a type structure like: ```tsx type Drink = Cocktail | Mocktail; ``` ## **What is `allOf`?** The `allOf` keyword in JSON Schema and OpenAPI combines multiple schemas to create a single object that must be valid against **all of** the given subschemas. `allOf` is the closest OpenAPI analog to an intersection type or a composite data type. You can use allOf to create a new type by combining multiple existing types. The new type has all the features of the existing types. ```json components: schemas: MealDeal: type: object allOf: - $ref: "#/components/schemas/Cocktail" - $ref: "#/components/schemas/Snack" ``` That would produce a type structure like: ```tsx type MealDeal = Cocktail & Snack; ``` ### Pitfall: Construction of Illogical Schemas `allOf` has valid use cases, but you can also shoot yourself in the foot fairly easily. The most common problem that occurs when using `allOf` is the construction of an illogical schema. Consider the following example: ```yaml type: object properties: orderId: description: ID of the order. type: integer allOf: - $ref: '#/components/schemas/MealDealId' ... components: schemas: MealDealId: type: string description: The id of a meal deal. ``` The OAS itself doesn't mandate type validation, so this is *technically* valid. However, if you try to turn this into functional code, you will quickly realize that you're trying to make something both an integer and a string at the same time, something that is clearly not possible. >Speakeasy's implementation of `allOf` is a work in progress. To avoid the construction of illogical types, we currently construct an object using the superset of fields from the listed schemas. In cases where the base schemas have a collision, we will default to using the object deepest in the list. ## **What is `anyOf`?** The `anyOf` keyword in JSON Schema and OpenAPI is the poor misunderstood sibling of `oneOf` and `allOf`. There is no established convention about how `anyOf` should be interpreted, which often leads to some very nasty unintended behavior. The issue arises when `anyOf` is interpreted to mean that a value must match **at least one** of the given listed schemas. >There could be a valid use of `anyOf` to describe an extended match of **one** element of a list. But that is not currently implemented by any OpenAPI tooling known to us. ### Pitfall: Combinatorial Explosion of Type `anyOf` leads to a lot of problems in code generation because, taken literally, it describes a combinatorial number of data types. Imagine the following object definition: ```tsx components: schemas: Drink: type: object anyOf: - $ref: "#/components/schemas/Soda" - $ref: "#/components/schemas/Water" - $ref: "#/components/schemas/Wine" - $ref: "#/components/schemas/Spirit" - $ref: "#/components/schemas/Beer" ``` >To avoid the explosion of types described below, Speakeasy's SDK creation interprets `anyOf` as `oneOf`. If you're doing code generation, you need to explicitly build types to cover all the possible combinations of these 5 liquids (even though most would be disgusting). That would lead you to build over 200 types to cover all the different combinations. That would lead to tremendous bloat in your library. That's why our recommendation is **don't use anyOf.** ## Describing Nullable Objects People sometimes incorrectly use `oneOf` when they want to indicate that it is possible for an object to be null. It differs based on the the version of OpenAPI you are using, but there are better ways to describe something as nullable. If you are using OpenAPI 3.0 use the nullable property: ```yaml components: schemas: Drink: type: object nullable: true ``` If you are using OpenAPI 3.1, use `type: ['object', ‘null']` to specify that an object is nullable: ```yaml components: schemas: Drink: type: [object, 'null'] ``` ## **Conclusion** AnyOf, AllOf, and OneOf are powerful keywords that can be used to define the structure and validation rules for data in OpenAPI. You'll notice that this article doesn't cover the JSON Schema `not`keyword. Although this keyword is valid in OAS, its use with code-generation tools leads to immediate problems. How can a code generator generate code for every possible schema **except** one or a set? This problem has taxed many big-brains, and remains unsolved today. Here is a link to a blog post that provides more information about defining data types in OpenAPI: https://speakeasy.com/post/openapi-tips-data-type-formats/ # openapi-tips-query-parameters-serialization Source: https://speakeasy.com/blog/openapi-tips-query-parameters-serialization import { Callout } from "@/mdx/components"; Hi! These blog posts have been popular, so we've built an entire [OpenAPI Reference Guide](/openapi/) to answer any question you have. It includes detailed information on [**query parameters**](/openapi/paths/parameters/query-parameters). Happy Spec Writing! ## The Problem The OpenAPI spec is best known for descriptions of RESTful APIs, but it'is designed to be capable of describing any HTTP API whether that be REST or something more akin to RPC based calls. That leads to the spec having a lot of flexibility baked-in: there's a lot of ways to achieve the exact same result that are equally valid in the eyes of the spec. Because of this, [the OpenAPI documentation](https://spec.openapis.org/oas/v3.1.0#operation-object) is very ambiguous when it comes to how you should define your API. That's why we're taking the time to eliminate some of the most common ambiguities that you'll encounter when you build your OpenAPI schema. In this case we'll be taking a look at **how to serialize query parameters in your OpenAPI 3.0.X schema.** ## Recommended Practices The OpenAPI spec grants quite a bit of flexibility in defining query parameters for any operation. There are many serialization options and defaults, therefore it's advisable you **define query parameters as strictly as possible** in your schema. This will **improve your API documentation** thereby reducing ambiguity for end-users. In addition, explicit definitions will **aid any OpenAPI tooling you may be using to produce artifacts**, such as client SDKs. As an API developer, strict definitions will also give you a more intuitive understanding of each operatio's intended behavior as you iterate on your OpenAPI schema. Concretely, we recommend that you: - Describe your query parameters as explicitly as possible by using OpenAPI defined formats. - Use additional validation attributes as much as possible: mark properties as required, allowReserved, allowEmptyValue, and indicate when fields are nullable. It's also important to note that OpenAPI considers a unique operation as a combination of a path and HTTP method, so it is not possible to have multiple operations that only differ by query parameters. In this case, it's advisable to use unique paths as shown below: ```yaml GET /users/findByName?name=anuraag GET /users/findByRole?role=developer ``` ## Query Parameters Query parameters are criteria which appear at the end of a request URL demarcated by a question mark (?), with different key=value pairs usually separated by ampersands (&). They may be required or optional, and can be specified in an OpenAPI schema by specifying **in: query**. Consider the following operation for an event catalog: ```yaml GET /events?offset=100&limit=50 ``` Query parameters could be defined in the schema as follows: ```yaml parameters: - in: query name: offset schema: type: integer description: The number of items to skip before starting to collect the result set - in: query name: limit schema: type: integer description: The numbers of items to return ``` When you're working with query parameters, it's important to understand serialization. Let's explore what serialization is, and the variety of ways the OpenAPI specification supports serialization of query parameters. ## Serialization Serialization is responsible for transforming data into a format that can be used in transit and reconstructed later. For query parameters specifically, this format is the query string for requests of that operation. The serialization method allows us to define this through the use of the following keywords: - **style** – defines how multiple values are delimited. Possible styles depend on the parameter location – [path](https://swagger.io/docs/specification/serialization/#path), [query](https://swagger.io/docs/specification/serialization/#query), [header](https://swagger.io/docs/specification/serialization/#header) or [cookie](https://swagger.io/docs/specification/serialization/#cookie). - **explode** – (true/false) specifies whether arrays and objects should generate separate parameters for each array item or object property. OpenAPI supports serialization of arrays and objects in all operation parameters (path, query, header, cookie). The serialization rules are based on a subset of URI template patterns defined by [RFC 6570](https://tools.ietf.org/html/rfc6570). From the OpenAPI Swagger documentation, query parameters support the following style values: - **form** (default): ampersand-separated values, also known as form-style query expansion. Corresponds to the `{?param_name}` URI template. - **spaceDelimited**: space-separated array values. Has effect only for non-exploded arrays (`explode: false`), that is, the space separates the array values if the array is a single parameter, as in arr=a b c. - **pipeDelimited**: pipeline-separated array values. Has effect only for non-exploded arrays (`explode: false`), that is, the pipe separates the array values if the array is a single parameter, as in arr=a|b|c. - **deepObject**: simple non-nested objects are serialized as `paramName[prop1]=value1¶mName[prop2]=value2&....` The behavior for nested objects and arrays is undefined. The **default serialization method** is **style: form and explode: true**. As shown in the GET /events call above, the `“?offset=100&limit=50”` query string is created with this default serialization when the schema has no references to style or explode. However, we recommend explicitly setting these values, even in the default case, to reap the benefits discussed in “Recommended Practices” above. Style and explode cover the most common serialization methods, but not all. For more complex scenarios (ex. a JSON-formatted object in the query string), you can use the **content** keyword and specify the media type that defines the serialization format. The example schema below does exactly that: ```yaml parameters: - in: query name: filter # Wrap 'schema' into 'content.' content: application/json: #media type indicates how to serialize/deserialize parameter content schema: type: object properties: type: type: string location: type: string ``` ## Additional Attributes Query parameters can be specified with quite a few additional attributes to further determine their **serialization, optionality, and nullability**. ### AllowReserved This is the only additional attribute which is specific to query parameters. From the OpenAPI Swagger documentation: The **allowReserved** keyword specifies whether the reserved characters, defined as :**/?#[]@!$&'()\*+,;=** by [RFC 3986](https://www.rfc-editor.org/rfc/rfc3986), are allowed to be sent as they are as query parameter values or should be percent-encoded. By default, allowReserved is false, and reserved characters are percent-encoded. For example, / is encoded as %2F (or %2f), so that the parameter value, events/event_info.txt, will be sent as events%2Fevent_info.txt. To preserve the / as is, **allowReserved** would have to be set to true as shown below: ```yaml parameters: - in: query name: path required: true schema: type: string allowReserved: true ``` ### Required By default, OpenAPI treats all request parameters as optional. You can add **required: true** to mark a parameter as required. ### Default Use the **default** keyword in the parameter schema to specify the default value for an optional parameter. The default value is the one that the server uses if the client does not supply the parameter value in the request. The value type must be the same as the parameter's data type. Consider a simple example, where default used with paging parameters allows these 2 calls from the client to be equivalent: ```yaml GET /events GET /events?offset=0&limit=100 ``` This would be specified in the schema like so: ```yaml - in: query name: offset schema: type: integer default: 0 description: The number of items to skip before starting to collect the result set - in: query name: limit schema: type: integer default: 100 description: The numbers of items to return ``` The **default keyword should not be used with required values**. If a parameter is required, the client must always send it and therefore override the default. ### Enum and Constant Parameters You can restrict a parameter to a fixed set of values by adding the enum to the parameter's schema. The **enum** values must be the same type as the parameter data type. A constant parameter can then be defined as a required parameter with only one possible value as shown below: ```yaml parameters: - in: query name: eventName required: true schema: type: string enum: - coachella ``` The enum property specifies possible values, and in this example, only one value can be used. It's important to note a **constant parameter is not the same as the default parameter value**. A constant parameter is always sent by the client, whereas the default value is something that the server uses if the parameter is not sent by the client. ### Empty-Valued and Nullable Query string parameters may only have a name and no value, like so: ```yaml GET /events?metadata ``` Use allowEmptyValue to describe such parameters: ```yaml parameters: - in: query name: metadata required: true schema: type: boolean allowEmptyValue: true ``` The OpenAPI spec also supports **nullable** in schemas, allowing operation parameters to have the null value when **nullable: true**. This simply means the parameter value can be null, and is **not the same as an empty-valued or optional parameter**. ### Deprecated Use **deprecated: true** to mark a parameter as deprecated. ### Common Parameters Across Methods in Same Path Parameters may be defined once to be used in multiple methods/paths in an OpenAPI schema. Parameters shared by all operations of a path can be defined on the path level instead of the operation level. These path-level parameters are inherited by all operations (GET/PUT/PATCH/DELETE) of that path. An example is shown below(manipulating the same resource in different ways is a good use case here): ```yaml paths: /events: parameters: - in: query name: filter content: application/json: schema: type: object properties: type: type: string location: type: string get: summary: Gets an event by type and location ... patch: summary: Updates the newest existing event with the specified type and location ... delete: ``` Any extra parameters defined at the operation level are used **in addition** to path-level parameters. Specific path-level parameters **may also be overridden on the operation level, but cannot be removed**. ### Common Parameters Across Multiple Paths Parameters can also be shared across multiple paths. Pagination is a good candidate for this: ```yaml components: parameters: offsetParam: - in: query name: offset required: false schema: type: integer minimum: 0 default: 0 description: The number of items to skip before collecting the result set. limitParam: - in: query name: limit required: false schema: type: integer minimum: 1 default: 10 description: The number of items to return. paths: /events: get: summary: Gets a list of events parameters: - $ref: '#/components/parameters/offsetParam' - $ref: '#/components/parameters/limitParam' responses: '200': description: OK /locations: get: summary: Gets a list of locations parameters: - $ref: '#/components/parameters/offsetParam' - $ref: '#/components/parameters/limitParam' responses: '200': description: OK ... ``` Note the above parameters defined in components are simply global definitions that can be handily referenced. They are not necessarily applied to all methods of an operation. # openapi-tips-webhooks-callbacks Source: https://speakeasy.com/blog/openapi-tips-webhooks-callbacks import { Callout, Table } from "@/mdx/components"; Hi! These blog posts have been popular, so we've built an entire [OpenAPI Reference Guide](/openapi/) to answer any question you have. It includes detailed information on [**webhooks**](/openapi/webhooks) **&** [**callbacks**](/openapi/webhooks/callbacks). Happy Spec Writing! ## What Are Webhooks and Callbacks and When Are They Used in OpenAPI? A traditional REST API functions by allowing users to trigger a request that the API immediately sends a response to; this is known as a pull mechanism. Webhooks and callbacks expand the possible ways of working with the API beyond this traditional call/response interface. - `webhooks` are a mechanism that allows an API to send real-time data to a user as soon as an event occurs (without requiring the user to take any action). The user simply needs to subscribe to the event stream and provide a URL to start receiving data. - `callbacks` are a mechanism that allows a user to specify a URL to which an API request should send a certain response. ### Which Should You Use? Both webhooks and callbacks are a way of defining asynchronous communication with an API. You can use webhooks and callbacks somewhat interchangeably, but we recommend sticking to the following convention: - If users will be receiving a stream of data over time with a consistent format, you should use webhooks. - If the initial API request triggers a long-running job and the user wants to receive the response asynchronously, use callbacks. ## A Short History of Webhooks and Callbacks in OpenAPI Callbacks were added to the OpenAPI Specification in [version 3](https://spec.openapis.org/oas/v3.0.0#callback-object) in 2017; webhooks in [version 3.1](https://spec.openapis.org/oas/v3.1.0#oasWebhooks) in 2021.
In OpenAPI, webhooks and callbacks are defined as follows: - `webhooks` are a top-level entry represented by a map of Path Item Objects or OpenAPI Reference Objects that are keyed by the unique name of the webhook. Webhooks specify what is pushed to a given URL but provide no way of setting the target URL. The subscription itself might be configured by a sales representative, entered during the sign-up process when a user creates an account on your system, or even set by a separate API endpoint (duplicating how callbacks work). - A `callback` is a map of runtime expressions (that represent a URL the callback request is sent to) to a Path Item Object or Reference that defines a request to be initiated by the API provider and a potential response to be returned. The expression, when evaluated at runtime, will resolve to a URL represented in the parameters, request body, or response body of the parent operation. A valid API schema can comprise only webhooks. For your schema to be valid, it needs only an `info` element and at least one of `paths`, `webhooks`, or `components`. ## Creating a Webhook in OpenAPI Below is the same notification service described as a webhook. ### Add a Webhook Description Add a webhook named `concertAlert` that describes what information will be pushed to a subscriber. This is the only requirement for a valid schema with a webhook. Notice that there is no way for a user to register for this alert using the API, nor is the URL on which the user will be notified specified. ```yaml filename="openapi.yaml" openapi: 3.1.0 info: title: SpeakeasyClub version: 1.0.0 webhooks: concertAlert: post: summary: Concert alert description: Notify the registered URL with details of an upcoming concert requestBody: required: true content: text/plain: schema: type: string example: "The Swing Machine will be playing at 19h30 tomorrow. $10 cover charge." responses: "200": description: Notification received by the external service. ``` ### Add a Subscription Path To allow users to register for alerts without using a callback, you can optionally mimic callback functionality by proving a subscription endpoint in a `/path`. OpenAPI has no way to explicitly link the webhook with the registration. You will have to make this clear to users in your `description` elements or by grouping the operations with tags. ```yaml filename="openapi.yaml" openapi: 3.1.0 info: title: SpeakeasyClub version: 1.0.0 webhooks: concertAlert: post: summary: Concert alert description: Notify the registered URL with details of an upcoming concert requestBody: required: true content: text/plain: schema: type: string example: "The Swing Machine will be playing at 19h30 tomorrow. $10 cover charge." responses: "200": description: Notification received by the external service. paths: /registerForAlert: post: summary: Register for concert alerts description: Register a URL to be called when new concerts are scheduled. requestBody: required: true content: application/json: schema: type: object properties: url: type: string format: uri description: The URL to be notified about approaching concerts. example: url: "http://example.com/notify" responses: "200": description: Registration successful. ``` ### Creating a Callback in OpenAPI Let's create a simple callback example. We use YAML rather than JSON for readability. ### Add a Subscription Path Now add a single `/registerForAlert` endpoint that a user can pass a URL to for concert notifications. We disregard error responses for brevity. ```yaml filename="openapi.yaml" openapi: 3.1.0 info: title: SpeakeasyClub version: 1.0.0 paths: /registerForAlert: post: summary: Register for concert alerts description: Register a URL to be called when new concerts are scheduled. requestBody: required: true content: application/json: schema: type: object properties: url: type: string format: uri description: The URL to be notified about approaching concerts. example: url: "http://example.com/notify" responses: "200": description: Registration successful. callbacks: concertAlert: "{$request.body#/url}": post: summary: Concert alert description: Notify the registered URL with details of an upcoming concert. requestBody: required: true content: text/plain: schema: type: string example: "The Swing Machine will be playing at 19h30 tomorrow. $10 cover charge." responses: "200": description: Notification received by the external service. ``` ## Add a Callback to the Path Finally, add a callback to the endpoint. The callback gets the URL using `{$request.body#/url}` from the POST to send a string describing the next concert. Selecting parameters in a POST request in this way is called a [runtime expression](https://spec.openapis.org/oas/v3.1.0#runtime-expressions). Read more about the syntax in the specification. ```yaml filename="openapi.yaml" openapi: 3.1.0 info: title: SpeakeasyClub version: 1.0.0 paths: /registerForAlert: post: summary: Register for concert alerts description: Register a URL to be called when new concerts are scheduled. requestBody: required: true content: application/json: schema: type: object properties: url: type: string format: uri description: The URL to be notified about approaching concerts. example: url: "http://example.com/notify" responses: "200": description: Registration successful. callbacks: concertAlert: "{$request.body#/url}": post: summary: Concert alert description: Notify the registered URL with details of an upcoming concert. requestBody: required: true content: text/plain: schema: type: string example: "The Swing Machine will be playing at 19h30 tomorrow. $10 cover charge." responses: "200": description: Notification received by the external service. ``` ## Syntax Rules The `webhooks` element has identical syntax to the `paths` element. Both are lists of [Path Item Objects](https://spec.openapis.org/oas/v3.1.0#pathItemObject). (This makes sense if you consider that a webhook is like a reverse path: Just as paths describe endpoints on the server's API, webhooks describe endpoints on the user's API.) A `callback` is also a Path Item Object. This means a webhook or callback has all the following path properties available to it: `$ref`, `summary`, `description`, `get`, `put`, `post`, `delete`, `options`, `head`, `patch`, `trace`, `servers`, and `parameters`. ## Callbacks and Webhooks in Speakeasy Speakeasy will [automatically include your webhook types](/docs/customize-sdks/webhooks) in generated code and documentation. You don't need to do anything extra or include Speakeasy extensions (`x-speakeasy`) to use these features. ## Tips Here are a few more considerations when designing your API's use of webhooks and callbacks: - If using callbacks, you can manage multiple subscriptions in the same endpoint using multiple parameters — one for each URL. This might be neater than creating one registration path per callback. - If using webhooks, be sure to explain in the summary or description exactly how users can subscribe and unsubscribe, as well as any associated fees for the service. - Any `description` elements may use [CommonMark syntax](https://spec.openapis.org/oas/v3.1.0#rich-text-formatting). - Callbacks and webhooks must have unique names in the schema. The names are case-sensitive. - Make your webhooks idempotent. Sending or receiving the same event multiple times should not cause side effects. - Protect your API from DDoS attacks by making sure webhooks are protected from spam registrations with authentication and that limits are in place for the rate and size of your messages. ## What About AsyncAPI Standard? OpenAPI is a general-purpose API specification that can be used for asynchronous APIs, but it is not necessarily optimized for them. If you find that OpenAPI is insufficient for your use case, you should check out [AsyncAPI](https://www.asyncapi.com/). Just be aware that AsyncAPI is still in the early stages of development and is not yet widely supported by the tooling ecosystem. # openapi-webhook-support Source: https://speakeasy.com/blog/openapi-webhook-support Where would we be without Webhooks? Inefficient polling leading to unnecessary load on our APIs. And although Webhooks are an important part of most companies with an API strategy, they often have a less robust Developer Experience than API endpoints. At Speakeasy, we're doing our part to make sure that webhooks aren't treated differently from other API endpoints. That's why we've added support for OpenAPI webhook definitions in our SDK generator. ## New Features **Webhook Support** - For each defined webhook in your OpenAPI spec, Speakeasy will generate the types for your webhook's request, and response objects. This makes sure that whatever the endpoint type, your users have a consistent experience interfacing with your API. **OpenAPI** ```yaml webhooks: newPet: post: requestBody: description: Information about a new pet in the system content: application/json: schema: $ref: "#/components/schemas/Pet" responses: "200": description: Return a 200 status ``` **SDK Output** ```go package webhooks import ( "openapi/pkg/models/shared" ) type NewPetRequest struct { Request *shared.Pet `request:"mediaType=application/json"` } type NewPetResponse struct { ContentType string StatusCode int64 } ``` ## Small Improvements **http.ServeMux Support** - For those interested in getting request & response logs by integrating Speakeasy server-side, we now offer support for any router based on http.ServeMux, for example: [httptrace.Mux](https://pkg.go.dev/gopkg.in/DataDog/dd-trace-go.v1/contrib/net/http). **“Does Not Contain” filters** - A nice quality of life improvement. When you're filtering logs in the Speakeasy request viewer, you can now use the “does not contain” operator to build your filters. # our-series-a-fundraise Source: https://speakeasy.com/blog/our-series-a-fundraise No Changelog this week. Just a big thank you to our customers, partners, and team for helping us reach this milestone. We're excited to continue building the future of developer experience with you all. [**Read more about our latest funding 🚀**](/post/fundraising-series-a) # pact-vs-openapi Source: https://speakeasy.com/blog/pact-vs-openapi The promise of reliable, well-tested APIs shouldn't at the expense of developer productivity. Yet, that's precisely what many teams face when Pact, a consumer-driven approach to enterprise contract testing. Pact's approach was born out of a worthy goal. Rigorous and comprehensive API contract testing is necessary, but Pact fell into a classic trap. The creators tried to create a new standard to solve 100% of the testing problem, instead of building on existing standards that already solved 80% of the issue: OpenAPI. The result was an extra 20% of value for at least 2x the cost (but typically much more). In this article, we'll weigh up the costs and benefits of using Pact vs. OpenAPI for API contract testing. TL;DR: 1. While Pact offers comprehensive contract testing, its implementation complexity and maintenance overhead often outweigh the benefits for most teams. 2. OpenAPI-based testing provides comparable reliability with significantly less overhead, especially when combined with Arazzo for workflow testing. 3. Teams serving external API consumers can benefit most from OpenAPI-based approaches, while Pact might still make sense for mission-critical dependencies within internal microservices architectures. ## The API testing challenge Well-tested APIs are essential when building reliable systems. API integrations, sitting at the edges between systems, represent the ideal place to catch and prevent issues before they cascade into production problems. Getting testing right at these boundaries provides outsized returns on investment. ### Why API integration failures are so expensive Bugs that originate with API requests typically propagate through multiple systems, making them extremely difficult to pinpoint and resolve. This leads to rolled-back releases, large-scale outages, and years of wasted developer time. The most expensive loss may very well be developers' trust in your system. To minimize the cost of API failures, teams employ various testing methodologies for their APIs. The most comprehensive test methodologies are usually the most complex. We need to ask whether adding more complexity to an already brittle integration may be pricier than simply supporting the bugs that arise. ### What teams want from tests To be truly useful, API testing strategies should provide three key benefits: 1. Confidence that API changes will not break existing integrations. 2. Early detection of potential issues. 3. **Minimal maintenance overhead.** Consumer-driven contract testing, as implemented with Pact, deliver on the first two points, but fails spectacularly at the third. ## Understanding Pact Pact implements consumer-driven contract testing, where API consumers define their expectations in advance. These expectations form a contract that API providers must fulfill. While this approach sounds logical, its implementation can become surprisingly complex. ```mermaid sequenceDiagram title Testing workflow with Pact participant Consumer Team participant Pact Broker participant Provider Team Note over Consumer Team: Write consumer test Consumer Team->>Consumer Team: Generate contract Consumer Team->>Pact Broker: Publish contract Note over Pact Broker: Store contract Provider Team->>Pact Broker: Fetch contracts Pact Broker->>Provider Team: Provider Team->>Provider Team: Verify API against contracts Provider Team->>Pact Broker: Publish results alt If contract verification fails Provider Team->>Consumer Team: Negotiate changes Consumer Team->>Consumer Team: Update tests Consumer Team->>Pact Broker: Republish contract end ``` Let's break down what's happening: ### 1. Consumer teams write tests describing their API expectations The consumer's teams must write specialized Pact tests in one of Pact's supported languages. These tests define expected request and response pairs. Here's an example from [pact-js](https://github.com/pact-foundation/pact-js): ```javascript import { PactV3, MatchersV3 } from '@pact-foundation/pact'; // Create a 'pact' between the two applications in the integration we are testing const provider = new PactV3({ dir: path.resolve(process.cwd(), 'pacts'), consumer: 'MyConsumer', provider: 'MyProvider', }); // API Client that will fetch dogs from the Dog API // This is the target of our Pact test public getMeDogs = (from: string): AxiosPromise => { return axios.request({ baseURL: this.url, params: { from }, headers: { Accept: 'application/json' }, method: 'GET', url: '/dogs', }); }; const dogExample = { dog: 1 }; const EXPECTED_BODY = MatchersV3.eachLike(dogExample); describe('GET /dogs', () => { it('returns an HTTP 200 and a list of dogs', () => { // Arrange: Setup our expected interactions // // We use Pact to mock out the backend API provider .given('I have a list of dogs') .uponReceiving('a request for all dogs with the builder pattern') .withRequest({ method: 'GET', path: '/dogs', query: { from: 'today' }, headers: { Accept: 'application/json' }, }) .willRespondWith({ status: 200, headers: { 'Content-Type': 'application/json' }, body: EXPECTED_BODY, }); return provider.executeTest((mockserver) => { // Act: test our API client behaves correctly // // Note we configure the DogService API client dynamically to // point to the mock service Pact created for us, instead of // the real one dogService = new DogService(mockserver.url); const response = await dogService.getMeDogs('today') // Assert: check the result expect(response.data[0]).to.deep.eq(dogExample); }); }); }); ``` ### 2. These tests generate a contract file When the developer runs their tests locally or in a staging environment, Pact generates a contract file. Contract files are JSON documents that describe the consumer's expected requests and responses. Each consumer generates their own contract. If your API has 100 consumers, you can expect 100 contract files to be generated by their teams. ### 3. The contract is published to a Pact Broker As the provider, you need to maintain a service called a Pact Broker. This service versions, tags, and stores contracts. It sits between the consumer and the provider. ### 4. Provider teams verify their API against all consumer contracts As part of the provider's CI/CD workflow, their API needs to verify against all consumer contracts in the Pact Broker. Here's an example from [pact-js](https://github.com/pact-foundation/pact-js): ```javascript const { Verifier } = require("@pact-foundation/pact"); // (1) Start provider locally. Be sure to stub out any external dependencies server.listen(8081, () => { importData(); console.log("Animal Profile Service listening on http://localhost:8081"); }); // (2) Verify that the provider meets all consumer expectations describe("Pact Verification", () => { it("validates the expectations of Matching Service", () => { let token = "INVALID TOKEN"; return new Verifier({ providerBaseUrl: "http://localhost:8081", // <- location of your running provider pactUrls: [ path.resolve(process.cwd(), "./pacts/SomeConsumer-SomeProvider.json"), ], }) .verifyProvider() .then(() => { console.log("Pact Verification Complete!"); }); }); }); ``` Each time the API is verified, the results are published back to the Pact Broker. ### 5. Failed verifications block provider deployments This is where this process changes from complex to a developer time vortex. The provider and consumer teams now have to negotiate contract changes. Either the provider must fix their API, or consumers need to update their tests and publish new contracts. This process takes place for all failed verifications across all consumer contracts. Finally, the provider re-verifies the API and the cycle starts anew. ### Pact's direct costs The Pact workflow above demonstrates why implementing Pact is often so complex: - **Setup complexity**: The initial implementation of Pact is a significant undertaking for any team size. It requires standing up new tools, and the orchestration of CI/CD pipelines. - **Coordination overhead**: The burden of testing lies with API consumers, but the maintenance and synchronization of contracts depends on the provider. This coordination requires meetings, training, and ongoing maintenance. - **Learning curve**: Pact requires specialized knowledge on both the consumer and provider's side. - **Misaligned incentives**: Pact separates the responsibility for the contract, from the responsibility of maintaining the service. It's hard to motivate the consumer teams to keep on top of maintaining Pact definitions when the services they are consuming are at the periphery of their day-to-day work. Out of sight, out of mind. - **Duplication**: Each consumer team has to maintain their own Pact tests with overlap between teams likely. In almost every case, OpenAPI offers comparable benefits without these costs. But before we dive into OpenAPI, let's take a look at where Pact **does** shine. ### Where Pact shines Large organizations with a multitude of internal microservices and strong inter-team communication channels may be able to make Pact work, regardless of the complexity and cost. Pact's high degree of assurance can also provide the confidence required to release updates to truly mission-critical services, where failures would be catastrophic. In that case, the cost of failure far outweighs the maintenance cost of Pact tests and workflows. The decision to use Pact should be based on potential costs of API failures versus the ever-increasing complexity of maintaining Pact tests. Now, let's take a look at OpenAPI as an alternative. ## The OpenAPI ecosystem approach While Pact focuses on consumer-driven contracts, OpenAPI is effectively the reverse: a provider-driven API spec that guarantees a contract to the consumer. This approach aligns the responsibility for the contract with the responsibility of maintaining the service. ```mermaid sequenceDiagram title Testing workflow with OpenAPI participant API Team participant Test Runner participant OpenAPI Spec participant Consumer Teams API Team->>Test Runner: Propose spec update Test Runner->>OpenAPI Spec: Test passed: publish OpenAPI Spec->>Consumer Teams: Consume API (directly or SDK) Note over Consumer Teams: Compile-time validation alt Breaking Change Test Runner->>API Team: Test failed: block release alt Manual release Test Runner->>OpenAPI Spec: Override Test: publish end end ``` ### Benefits of using OpenAPI for contract testing The OpenAPI approach offers several benefits over Pact: 1. **Reduced setup complexity**: OpenAPI specifications are easier to write and maintain than Pact tests. They can also be generated automatically from some API frameworks. Most orgs already have an OpenAPI specification for their API, so their is no setup cost. 2. **Little to no coordination needed between teams**: Since the API team owns the OpenAPI specification, consumers use the API as documented. There's no need for separate contracts or negotiation between teams. 3. **No duplication**: There is a single OpenAPI specification for every consumer. There's no need for per-team contracts. 4. **Early detection**: You can test changes to your API before writing code. You simply propose a change to the OpenAPI specification, and run your tests. ### Limitations of using OpenAPI for contract testing OpenAPI is not specifically designed for contract testing. It's a specification for describing your API, and as such, it's not as comprehensive as Pact. 1. **Coverage scope**: OpenAPI-based testing is best suited for testing the API's contract and functionality. Natively, it may not catch all integration issues, especially those that involve multiple APIs or complex business logic. (More on this in the next section.) 2. **Specification maintenance**: OpenAPI specifications must be kept up-to-date with the API's actual behavior. If the specification falls out of sync with the API, consumers may encounter unexpected behavior. However, this is a common problem with any API testing strategy. 3. **Theoretical vs. actual**: In some ways, an OpenAPI spec is a theoretical contract. It describes all the ways the API can be used, but doesn't describe how the API is actually used in practice. This can lead to false positives in your tests. There may be a breaking change in the API, but in actual practice, no consumer is not using the API in that way. ## Future considerations [Arazzo](/openapi/arazzo) is a recently published workflow specification that enables developers to describe complex workflows that involve multiple APIs. By combining OpenAPI-based testing with Arazzo, teams can [test end-to-end scenarios that span multiple APIs](/post/e2e-testing-arazzo), ensuring that their integrations work as expected. ### How Arazzo workflows enhance API testing 1. **End-to-end testing**: Arazzo workflows describe complex interactions between multiple APIs, enabling teams to test entire user journeys from start to finish. 2. **Scenario-based testing**: Workflows can describe different scenarios that users might encounter, such as error conditions, edge cases, or performance bottlenecks. 3. **Automated testing**: Workflows can be executed automatically as part of your CI/CD pipeline, ensuring that your integrations work as expected before they reach production. 4. **Improved developer experience**: By describing workflows in a human-readable format, Arazzo makes it easy for developers to understand and contribute to your testing efforts. We foresee a future where teams combine OpenAPI-based testing with Arazzo workflows to create comprehensive, reliable, and maintainable API tests that catch integration issues early and often. ## Making the decision When deciding between Pact and OpenAPI-based testing, consider the following factors: 1. **Team structure** - Teams with mission-critical internal dependencies might prefer Pact. - Smaller or more distributed teams could benefit more from OpenAPI's reduced complexity. 2. **Consumer profile** - If your APIs have many external consumers, OpenAPI is the better choice as it provides a single contract for every consumer. - APIs with few but tightly coupled internal consumers might justify Pact's effort for higher assurance. 3. **Resource availability** - Teams with limited resources may find OpenAPI more manageable due to lower overhead and maintenance. - Pact may require dedicated resources for broker management and contract maintenance. 4. **API complexity** - Straightforward APIs with straightforward interactions may not need the advanced features of Pact; OpenAPI suffices. - Complex APIs with functionality that lives outside the spec may still benefit from Pact's guarantees. The choice between Pact and OpenAPI should align with your team's structure, goals, and resources. By understanding the trade-offs, you can make an informed decision that best suits your API strategies. ## Key differences between Pact and OpenAPI - **Pact**: Offers thorough contract testing with a heavy emphasis on consumer-driven contracts but involves significant setup and maintenance complexity. - **OpenAPI**: Provides a single contract for every consumer, and a single source of truth for your API. It is lightweight to implement, and requires no additional setup at the cost of some comprehensiveness. ## How to try OpenAPI-based testing Speakeasy offers a simple way to get started with OpenAPI-based testing: Start by creating an OpenAPI specification for your API, or add examples to your existing specification. That's all there is to it. With Speakeasy, you can start testing your API with generated SDKs and tests in minutes. If you made it this far, you're clearly interested in improving your API testing strategy. We're on a mission to solve this problem, and we'd love to hear from you. [Join our Slack community](https://go.speakeasy.com/slack) to share your thoughts, or to learn more about Speakeasy. # php-beta-sso-for-enterprises Source: https://speakeasy.com/blog/php-beta-sso-for-enterprises import { Callout } from "@/lib/mdx/components"; [Laravel raised $57M in Series A funding](https://laravel-news.com/laravel-raises-57-million-series-a) 💰 making it clear that PHP is back in a big way. That makes it the perfect time to announce the beta release of our new PHP SDK Generator! We're bringing modern type safety and a streamlined developer experience to PHP SDKs generated on our platform. And in less splashy (but still important) news, we're launching Single Sign-On (SSO) support on the Speakeasy platform! Read on for more details. ## Type-Safe PHP Generation is now in Beta ```php declare(strict_types=1); require 'vendor/autoload.php'; use Dub; use Dub\Models\Components; use Dub\Models\Operations; $security = new Components\Security( token: "DUB_API_KEY", ); $sdk = Dub\Dub::builder()->setSecurity($security)->build(); try { $request = new Operations\CreateLinkRequestBody( url: 'https://google.com', tagIds: '...', externalId: '123456', ); $response = $sdk->links->create($request); if ($response->linkSchema !== null) { // handle response } } catch (Throwable $e) { // handle exception } ``` Here's a quick rundown of what our new PHP SDK Generator brings to the table: - **Robust type safety** with carefully typed properties for all models - **Support for union types**, embracing PHP 8's modern type system - **Readability**, a streamlined, object-oriented approach for easy debugging - **Minimal external dependencies** for a lightweight footprint - **IDE compatibility** for a superior development experience We can't wait to hear what people think! Please don't hesitate to reach out with feedback and questions. [Read the release post here](/post/release-php). --- ## SSO for Enterprises ![SSO](/assets/changelog/assets/sso.png) We're excited to announce the launch of Single Sign-On (SSO) support on the Speakeasy platform. Our SSO is compatible with any IDP that uses SAML or OIDC (i.e. Okta). We're committed to providing robust, enterprise-grade solutions to businesses of every size. If you're interested in enabling SSO for your organization, please reach out to your account manager for access. --- ## 🐝 New features and bug fixes 🐛 Based on the most recent CLI version: [**Speakeasy v1.398.0**](https://github.com/speakeasy-api/speakeasy/releases/tag/v1.398.0) ### Generation platform 🐝 Feat: initialize `git` repository during quickstart ### Python 🐝 Feat: upgrade to Pydantic 2.9 \ 🐛 Fix: ensure async client is used for async request building \ 🐛 Fix: handle additional properties in models with nullable fields \ 🐛 Fix: add usage snippets for `next` pagination func ### Go 🐛 Fix: Handle default streaming responses in go ### Terraform 🐝 Feat: add `resources` & `data sources` to terraform documentation ### PHP 🐝 Feat: add multi-level tagging support \ 🐝 Feat: add nullable support \ 🐛 Fix: improve handling of associative arrays contained in unions # php-general-availability-improved-fastapi-support-and-a-new-billing-page-experience Source: https://speakeasy.com/blog/php-general-availability-improved-fastapi-support-and-a-new-billing-page-experience import { Callout, ReactPlayer } from "@/lib/mdx/components"; We're excited to announce three significant updates: **PHP SDK Generation** has reached General Availability (GA), we've greatly improved **FastAPI spec support**, and we've introduced a refreshed **billing page experience**. --- ## 🚀 PHP is now Generally Available Following a successful beta period and renewed enthusiasm within the PHP ecosystem, we're thrilled to announce that Speakeasy's PHP SDK Generation is now generally available. This marks a major milestone in our mission to provide developers with robust, type-safe SDKs across all major programming languages. ### Key highlights: - **True type safety** with PHP 8's robust type system. - **Native support for BigInt & Decimal** with arbitrary precision. - **Built-in OAuth 2.0 flows** with automatic token management. - **Seamless Laravel integration** with ready-to-use Service Providers. - **Enhanced error handling** with specialized exception types. - **Intelligent pagination** for handling large datasets. 📖 [**Read the detailed release post for deeper insights into new features, code examples, real-world use cases, and much more!**](/post/release-php-ga) --- ## ⚡ Better Support for FastAPI Specs FastAPI has quickly become a favorite among Python developers due to its intuitive design, powerful data validation, and impressive performance. Yet, effectively generating SDKs from FastAPI specifications has often required manual tweaks and workarounds. Our latest improvements eliminate these hurdles, providing robust, automated integration for FastAPI specs, ensuring greater accuracy and significantly simplifying your workflow. --- ## 💳 New Billing Page Experience
Managing your add-ons is now simpler than ever. The redesigned billing page in your dashboard includes intuitive toggles for quickly managing feature access, providing clearer insights into your billing details and usage. --- ## 🛠️ New Features and Bug Fixes 🐛 Based on the most recent CLI version: [**Speakeasy v1.523.1**](https://github.com/speakeasy-api/speakeasy/releases/tag/v1.523.1) ### Platform 🐛 **Fix:** Improved naming resolution and JSON serialization performance.\ 🐝 **Feat:** Added MCP prompts and enhanced cursor pagination.\ 🐝 **Feat:** Enabled deduplication of inline error schemas.\ 🐝 **Feat:** Language server is now accessible via WebSocket. ### Ruby 🐝 **Feat:** Added HTTP retry capabilities, global timeout configurations, and customizable SDK hooks. ### TypeScript 🐛 **Fix:** Improved handling of duplex streams and OAuth2 security. ### Python 🐛 **Fix:** Disabled unnecessary JSON key sorting. ### Go 🐛 **Fix:** Improved annotations and reserved keyword handling. # php-sdks-now-available Source: https://speakeasy.com/blog/php-sdks-now-available PHP is like the cockroach of the programming ecosystem, people love to hate on it and yet it will likely outlive all of us. Some 80% of websites use PHP, with especially strong use in E-commerce & finance. So if you're building an API, support for the PHP community should be at the top of your priority list. Which is why we're excited to announce that Speakeasy now supports PHP SDK creation! ## PHP SDK Creation As with all Speakeasy SDKs, our PHP SDKs place an emphasis on being idiomatic and type-safe. We've made several choices to give PHP users an experience they would expect: - **Type hints & doc strings** wherever possible to guide the end user to use the right data types. - [**Jms/serializer**](https://jmsyst.com/libs/serializer) to accurately serialize and deserialize json bodies - [**Guzzle**](https://docs.guzzlephp.org/en/stable/) as the HTTP client of choice. However, like all our SDKs, we make it easy to swap in a custom client to be used if desired. PHP is live the Speakeasy CLI, so just upgrade to the latest version and create a great SDK for your PHP users: ```bash speakeasy quickstart ``` And here's some example output from the canonical Petstore example API for an `addPet` method: ```php /** * addPet - Creates a new pet in the store. Duplicates are allowed */ public function addPet(\PetstoreAPI\models\operations\AddPetRequest $request): \PetstoreAPI\models\operations\AddPetResponse { $baseUrl = $this->_serverUrl; $url = utils\Utils::generateURL($baseUrl, '/pets'); $options = ['http_errors' => false]; $body = utils\Utils::serializeRequestBody($request); if (is_null($body)) { throw new \Exception('Request body is required'); } $options = array_merge_recursive($options, $body); $client = $this->_defaultClient; $httpRes = $client->request('POST', $url, $options); $contentType = $httpRes->getHeader('Content-Type')[0] ?? ''; $res = new \PetstoreAPI\models\operations\AddPetResponse(); $res->statusCode = $httpRes->getStatusCode(); $res->contentType = $contentType; if ($httpRes->getStatusCode() == 200) { if (utils\Utils::matchContentType($contentType, 'application/json')) { $serializer = utils\JSON::createSerializer(); $res->pet = $serializer->deserialize($httpRes->getBody()->__toString(), 'PetstoreAPI\models\shared\Pet', 'json'); } } else { if (utils\Utils::matchContentType($contentType, 'application/json')) { $serializer = utils\JSON::createSerializer(); $res->error = $serializer->deserialize($httpRes->getBody()->__toString(), 'PetstoreAPI\models\shared\Error', 'json'); } } return $res; } ``` # picking-a-javascript-api-framework Source: https://speakeasy.com/blog/picking-a-javascript-api-framework import { Table } from "@/mdx/components"; The fast-paced and ever-evolving JavaScript ecosystem, with its plethora of server-side frameworks for building REST APIs in JavaScript and TypeScript, can feel overwhelming when choosing the right tool for your project. This article explores the key factors for selecting a JavaScript API framework, covering community support, documentation, scalability, request-processing speed, concurrency, and OpenAPI compatibility, and provides a decision-making framework to help you choose between options like Express, NestJS, Fastify, Hono, and ElysiaJS. ## Factors to consider when choosing a JavaScript API framework --- ### Iteration speed Speed is critical for startups and projects with tight deadlines. Selecting a JavaScript framework with a strong ecosystem ensures you can quickly build features and abstract logic without unnecessary complexity. Frameworks that support rapid development and scalability are ideal for MVPs. For quick iteration and simplicity, consider frameworks like Express or Hono, which enable rapid endpoint creation while leaving room for future enhancements. ### Robustness and security If your MVP has succeeded, or you need to build a secure API for an existing service, robustness and security take priority. To ensure a framework meets these needs, consider the following: - Architecture: Look for clear patterns, like dependency injection or layered architectures, to ensure modularity, scalability, and maintainability. - Community support: Opt for frameworks with active communities, comprehensive documentation, and a strong ecosystem of plugins. - Maintenance: Consider frameworks with stable or LTS versions, regular updates, and consistent security patches. - TypeScript support: Strong typing and improved refactoring capabilities make TypeScript invaluable for reducing runtime errors in large-scale projects. - Security features: Built-in authentication, access control, and data validation tools simplify secure API implementation and reduce vulnerability risks. ### Maturity vs innovation Bun has reshaped the JavaScript ecosystem by addressing the performance limitations often associated with Node.js. Known for its speed and optimization, Bun reduces the bottlenecks caused by slow Node.js core improvements. Bun offers faster and safer development with native optimizations and built-in tools, including a JavaScript runtime, bundler, and task runner. However, compared to Node.js, which boasts over 15 years of stability, long-term support policies, and a vast ecosystem, Bun lacks maturity and comprehensive compatibility. Despite this, Bun is steadily improving its compatibility with Node.js libraries, enabling developers to explore its potential. While frameworks like Express are compatible with Bun, Hono and ElysiaJS are specifically designed to leverage Bun's capabilities, offering streamlined integration and optimized performance. **So, how do you choose between a newer framework and a more established one?** If stability is essential and you want to avoid tools with an underdeveloped ecosystem or undocumented edge cases, opt for mature frameworks like Fastify, NestJS, or Express. However, ElysiaJS is worth testing if you already have experience with Bun's ecosystem. While robust, it's best suited for developers who are comfortable taking risks and delving into source code to address issues, as error handling and documentation might be limited. ## Popular JavaScript API frameworks --- ### Express: Lightweight, simple, and battle-tested [Express](https://expressjs.com/) is one of the oldest and most widely used JavaScript frameworks for building REST APIs. Valued for its simplicity and minimalism, Express provides a lightweight structure that allows developers to make their own architectural decisions. Its flexible design makes it ideal for projects with evolving or undefined requirements. Here's an example of creating an endpoint to fetch a list of products from a database: ```javascript filename="index.js" import express from "express"; import { Pool } from "pg"; // Create an Express app const app = express(); // PostgreSQL database connection pool const pool = new Pool({ connectionString: 'postgres://user:password@localhost:5432/mydatabase', }); // One-liner route to get all products app.get('/products', async (req, res) => { try { res.json((await pool.query('SELECT * FROM products')).rows); } catch (error) { console.error('Error fetching products:', error.message); res.status(500).json({ error: 'Internal Server Error' }); } }); // Start the server const PORT = 3000; app.listen(PORT, () => { console.log(`🚀 Server is running at http://localhost:${PORT}`); }); ``` You can start the server with the `node index.js` command. This example demonstrates a basic Express application, but you can organize your code or leverage ecosystem tools to streamline development. Express relies on the middleware design pattern, where middleware processes requests, handles tasks like logging, authentication, or error handling, and passes control to the next middleware or route handler. ![Express middleware architecture example](/assets/blog/picking-a-javascript-api-framework/express-middleware.png) Find a list of middleware modules the Express team maintains [in the documentation](https://expressjs.com/en/resources/middleware.html). ### Hono: For serverless APIs with essential features [Hono](/openapi/frameworks/hono) is a modern JavaScript framework tailored for serverless architectures that simplifies development by directly bundling features like authentication, middleware, and validation into the framework. **Why choose serverless for your API?** Serverless architecture reduces developer overhead by abstracting server management. Here are three technical benefits: 1. Cost efficiency: Pay only for your API's compute power, making it ideal for MVPs or traffic with unpredictable demand. 2. Dynamic scaling: Platforms like AWS Amplify and Cloudflare Workers scale automatically during traffic spikes without manual intervention. 3. Reduced complexity: Serverless manages infrastructure, patching, and scaling, letting developers focus on code. Hono enhances these serverless advantages by offering built-in authentication, validation, and routing tools, minimizing external dependencies, and streamlining development. Here is how you can write an API that returns a list of Pokémon, for example: ```javascript filename="index.js" import { Hono } from 'hono'; import { logger } from 'hono/logger'; import { cors } from 'hono/cors'; import { swaggerUI } from '@hono/swagger-ui' const app = new Hono(); // Pokémon data const pokemons = [ { id: 1, name: 'Bulbasaur', type: 'Grass/Poison' }, { id: 2, name: 'Charmander', type: 'Fire' }, { id: 3, name: 'Squirtle', type: 'Water' }, ]; // Middleware: Logger app.use('*', logger()); // Middleware: CORS app.use('*', cors()); // Middleware: Swagger UI app.get('/ui', swaggerUI({ url: '/doc' })) // Route to return a list of Pokémon app.get('/pokemons', (c) => { return c.json(pokemons); }); export default app; ``` ### NestJS: For Robust and Secure Enterprise Applications As your project scales or if you need a robust and scalable solution from the start, frameworks with strong architectural foundations are the better choice, offering proven design patterns and built-in tools to effectively address security, reliability, and scalability. The framework that excels in building robust, scalable, maintainable applications is [NestJS](/openapi/frameworks/nestjs). Its opinionated architecture enforces best practices, making NestJS ideal for complex systems focused on security and reliability. Built on top of Express or, optionally, Fastify, NestJS provides a structured layer tailored to enterprise needs. NestJS supports multiple programming paradigms: - Object-oriented programming (OOP): Ensures modularity and encapsulation. - Functional programming (FP): Promotes clean, declarative logic. - Domain-driven design (DDD): Suitable for large, complex applications with intricate domain modeling. NestJS's flexibility allows developers to handle simple CRUD operations and advanced business logic without compromising maintainability. Key built-in NestJS features that simplify development are: - Authentication and authorization: Use [@nestjs/jwt](https://github.com/nestjs/jwt) for authentication and implement role-based access control (RBAC) with guards for enhanced security. - OpenAPI documentation: [@nestjs/swagger](https://docs.nestjs.com/openapi/introduction) automatically generates OpenAPI-compliant documentation, easing API consumption and integration. - Deep TypeScript support: NestJS employs TypeScript's type system to provide robust type checking and ensure type safety throughout the application. However, these benefits come with a significant inconvenience: verbosity. Let's see how we can create a simple API to manage books for a library. First, you need to define the model. NestJS integrates well with Mongoose. ```typescript filename="book.schema.ts" import { Prop, Schema, SchemaFactory } from '@nestjs/mongoose'; import { Document } from 'mongoose'; @Schema() export class Book extends Document { @Prop({ required: true }) title: string; @Prop({ required: true }) author: string; @Prop() publishedYear: number; } export const BookSchema = SchemaFactory.createForClass(Book); ``` Then, you need to define the data transfer object (DTO), which helps validate incoming data. ```javascript filename="create-book.dto.ts" export class CreateBookDto { readonly title: string; readonly author: string; readonly publishedYear?: number; } ``` Next, you implement the service as the middleman between your application and the database. Queries to create and retrieve items can be defined here. ```javascript filename="books.service.ts" import { Injectable } from '@nestjs/common'; import { InjectModel } from '@nestjs/mongoose'; import { Model } from 'mongoose'; import { Book } from './book.schema'; import { CreateBookDto } from './create-book.dto'; @Injectable() export class BooksService { constructor(@InjectModel(Book.name) private readonly bookModel: Model) {} async getAllBooks(): Promise { return this.bookModel.find().exec(); } async createBook(createBookDto: CreateBookDto): Promise { const newBook = new this.bookModel(createBookDto); return newBook.save(); } } ``` With the services defined, you can write the controller that will handle the requests to the REST API. ```javascript filename="books.controller.ts" import { Controller, Get, Post, Body } from '@nestjs/common'; import { BooksService } from './books.service'; import { CreateBookDto } from './create-book.dto'; import { Book } from './book.schema'; @Controller('books') export class BooksController { constructor(private readonly booksService: BooksService) {} @Get() async getAllBooks(): Promise { return this.booksService.getAllBooks(); } @Post() async createBook(@Body() createBookDto: CreateBookDto): Promise { return this.booksService.createBook(createBookDto); } } ``` The `BooksController` will expose these endpoints: - `GET /books`: Retrieve all books. - `POST /books`: Add a new book. The defined components are part of the books module, which registers the providers (services), controllers, and schema, ensuring modularity and organization within the application. ```javascript filename="books.module.ts" import { Module } from '@nestjs/common'; import { MongooseModule } from '@nestjs/mongoose'; import { BooksController } from './books.controller'; import { BooksService } from './books.service'; import { Book, BookSchema } from './book.schema'; @Module({ imports: [MongooseModule.forFeature([{ name: Book.name, schema: BookSchema }])], controllers: [BooksController], providers: [BooksService], }) export class BooksModule {} ``` Finally, you can add the `BooksModule` to the main module. ```javascript filename="app.module.ts" import { Module } from '@nestjs/common'; import { MongooseModule } from '@nestjs/mongoose'; import { BooksModule } from './books/books.module'; @Module({ imports: [ MongooseModule.forRoot('mongodb://localhost/nest-books'), BooksModule, ], }) export class AppModule {} ``` NestJS's structured approach, though initially tedious, is justified by its ability to simplify the management of complex applications, even in large teams. Its modular design ensures that every component – models, DTOs, controllers, and modules – has a clear and defined role, reducing ambiguity and enforcing best practices. This focus on robust architecture and built-in features makes NestJS a trusted choice for critical applications, as evidenced by its use in organizations like [Adidas](https://adidas.github.io/) and Société Générale. When robustness and security are essential, NestJS provides the reliability needed. ### Fastify: A replacement for Express JavaScript's asynchronous core makes it inherently fast, but not all frameworks maximize its potential. A notable limitation of Express is its lack of optimization for asynchronous workflows. Although Express now supports `async/await`, its core, originally designed for callbacks, doesn't handle errors in async route handlers automatically. For example, if an `async` route handler throws an error or rejects a promise, you must explicitly use a `try/catch` block or pass errors to `next()` to invoke the built-in error-handling mechanism. Without this, unhandled errors may crash the application, highlighting Express's limitations in modern asynchronous processing. ```javascript app.get('/products', async (req, res, next) => { try { const data = await someAsyncFunction(); res.json(data); } catch (err) { next(err); // Pass the error to the Express error handler } }); ``` Without `try/catch` or `next(err)`, the application might crash or fail to handle the error appropriately. In addition, older third-party middleware in Express may not always support `async/await` out of the box, which can lead to performance bottlenecks, as the architecture doesn't optimize for high-throughput asynchronous tasks. This is where [Fastify](/openapi/frameworks/fastify) is a framework worth considering, offering syntax and ease of use that closely resemble Express. To illustrate, here is how the example we wrote for the product REST API built with Express can be implemented with Fastify: ```javascript filename="index.js" import Fastify from 'fastify'; import { createPool } from './db.js'; // Create a Fastify instance const fastify = Fastify({ logger: true }); // Import PostgreSQL connection pool const pool = createPool(); // Route to get all products fastify.get('/products', async (request, reply) => { try { const { rows } = await pool.query('SELECT * FROM products'); reply.send(rows); } catch (error) { fastify.log.error(error); reply.status(500).send({ error: 'Internal Server Error' }); } }); // Start the server const startServer = async () => { try { await fastify.listen({ port: 3000 }); console.log('🚀 Server is running at http://localhost:3000'); } catch (err) { fastify.log.error(err); process.exit(1); } }; startServer(); ``` The `GET /products` route can also be written using `fastify.route`. This method helps with schema definition for both the response and request and integrates with Fastify's built-in support for OpenAPI documentation generation if needed. ```javascript // Define the schema for the route const getProductsSchema = { response: { 200: { type: 'array', items: { type: 'object', properties: { id: { type: 'integer' }, name: { type: 'string' }, price: { type: 'number' }, description: { type: 'string' }, }, required: ['id', 'name', 'price'], }, }, }, }; // Define the route fastify.route({ method: 'GET', url: '/products', schema: getProductsSchema, handler: async (request, reply) => { try { const { rows } = await pool.query('SELECT * FROM products'); reply.send(rows); } catch (error) { fastify.log.error(error); reply.status(500).send({ error: 'Internal Server Error' }); } }, }); ``` Fastify excels in high-performance server setups by using a refined asynchronous model. Fastify, built on `async/await` and Promises, ensures non-blocking request handling with exceptional efficiency. Fastify's key features include: * Schema validation: Streamlines request and payload validation, reducing runtime overhead. * Logging performance: Integrated with [Pino](https://github.com/pinojs/pino) at its core, Fastify provides fast, low-cost logging even under heavy loads. * Throughput: Fastify can handle up to 30,000 requests per second, making it one of the fastest frameworks for traditional or containerized setups. That's [2x to 3x time faster than ExpressJS](https://thenewstack.io/a-showdown-between-express-js-and-fastify-web-app-frameworks/). Fastify's speed also comes from using the `fast-json-stringify` library for efficient JSON handling and a lightweight, plugin-based architecture that optimizes components and connections. Thanks to its high performance and plugin-based architecture, Fastify is an excellent choice for building fast, lightweight MVPs as it simplifies initial development. Fastify can serve as the HTTP adapter for a more robust framework, like NestJS, making it a strategic choice: Start with Fastify for speed and agility, then migrate to NestJS when requirements for modularity, maintainability, and advanced features like dependency injection and guards become priorities. ### ElysiaJS: For speed and developer experience With the emergence of Bun, the latest and fastest JavaScript runtime, new frameworks and paradigms are redefining the ecosystem. This raises the question: Should you choose a mature or cutting-edge framework? [ElysiaJS](https://elysiajs.com/) is a notable framework optimized for Bun and Node.js environments. Designed for high performance and developer experience, ElysiaJS includes features like route schema definitions that efficiently generate API documentation using tools such as Swagger UI or Scalar, providing a modern, responsive interface for managing APIs. Here's an example of an API written in ElysiaJS with OpenAPI schema definitions: ```javascript filename="index.js" import process from 'node:process' import { Elysia } from 'elysia' import { logger } from '@bogeychan/elysia-logger' import { swagger } from '@elysiajs/swagger' import { cors } from '@elysiajs/cors' import { prisma } from './db' import { getAllProducts } from './orm' const app = new Elysia() .use(logger()) .use( swagger({ documentation: { info: { title: 'Products API', version: '1.0.0', description: 'API documentation for retrieving all products.', }, servers: [ { url: 'http://localhost:3000', }, ], tags: "Other", components: { schemas: { Product: { type: 'object', properties: { id: { type: 'integer', }, name: { type: 'string', }, shopId: { type: 'integer', }, }, }, }, }, }, }), ) .use(cors()) .get( '/api/products', async () => { const products = await getAllProducts() return products }, { detail: { tags: ['Products'], summary: 'Get all products', }, }, ) await prisma.$connect() app.listen(process.env.PORT as string, () => console.info(`🦊 Server started at ${app.server?.url.origin}`)) ``` The code above exposes a `GET /api/products` API route but also does something interesting: It generates documentation built on [Scalar UI](https://scalar.com/). Visiting `https://localhost:3000/swagger` will return a page similar to this: ![ElysiaJS Scalar UI](/assets/blog/picking-a-javascript-api-framework/elysiajs-scalar.png) ## Making pragmatic choices The JavaScript API frameworks discussed here each have distinct strengths and trade-offs, making it challenging to select the right one for your project. We've included a flowchart outlining key factors like performance, scalability, and stability and a decision table that maps everyday use cases to recommended frameworks to assist in this decision. Use the flowchart to narrow your choices based on your project's needs and priorities. Then, consult the decision table to find the framework that best matches your requirements. ![Flowchart to choose a JavaScript API framework](/assets/blog/picking-a-javascript-api-framework/framework-js-selection.png)
To be concise, consider: - Stability or speed? Express and NestJS for stability; Fastify and ElysiaJS for speed. - New or scaling? Hono and Fastify for new projects; NestJS for scaling. - Features vs risk? NestJS provides robust features; Hono and ElysiaJS are lightweight and modern. - Team expertise? Express suits familiar teams; ElysiaJS is ideal for Bun experts. For stability, choose Express or NestJS; explore Fastify, Hono, or ElysiaJS for speed and innovation. # playwright-tool-proliferation Source: https://speakeasy.com/blog/playwright-tool-proliferation Flask creator, Armin Ronacher, [recently voiced a frustration](https://x.com/mitsuhiko/status/1942531115371131270) familiar to many MCP users: ![Tweet from Armin Ronacher](/assets/mcp/playwright-tool-proliferation/armin-tweet.png) When asked which tools he actually uses, [Armin's response was telling](https://x.com/mitsuhiko/status/1942533476592300215): > Most of the time it's navigate, press key, handle dialog, click, type, select, wait for, page snapshot. That's only eight of the 26 tools available on the Playwright MCP server. ## What is Playwright? [Playwright](https://playwright.dev) is a framework that allows AI agents to access a browser. Microsoft built it as a "universal remote control" for web browsers, with which you can control Chrome, Firefox, Safari, and Edge using the exact same code. Originally designed for testing web applications, Playwright has been extended with an MCP server that allows AI agents to interact with websites programmatically. What makes Playwright special is its reliability. Traditional browser automation tools suffer from flaky tests that fail randomly, usually because they don't wait for pages to load fully. Playwright solves this by automatically waiting for elements to be ready before interacting with them. It can also handle complex scenarios like multi-tab browsing, mobile device emulation, and even intercepting network requests to mock APIs. But here's the thing: Playwright was built for developers writing code, not for AI agents making decisions on the fly. That's where the MCP server comes in. ### How the Playwright MCP server actually works The Playwright MCP server acts as a bridge between AI agents and Playwright's browser usage capabilities. Instead of Claude Desktop trying to figure out Playwright's complex APIs directly, the MCP server translates requests from the agent into browser actions and returns the resultant state back to the browser. ![Playwright MCP architecture](/assets/mcp/playwright-tool-proliferation/playwright-mcp-architecture.png) The diagram above shows how Claude connects to your browser through the Playwright MCP server. Here's the flow when you ask Claude to _"Click the login button"_: 1. **Claude** (the MCP client) has access to the **Tools** section, which exposes browser automation capabilities like Navigate, Click, and Snapshot. 2. Claude selects the appropriate tool (in this case, Click) and sends an **action** to the **Playwright MCP** server. 3. The MCP server translates this into commands for the **Playwright framework**. 4. Playwright **interacts with** your actual **browser** to perform the click. 5. The browser sends a **response** to Claude through the same chain. Claude never directly touches the browser; it just selects from a curated set of tools that the MCP server exposes and manipulates the browser through the Playwright library. ## The tool proliferation problem To understand why Armin Ronacher felt overwhelmed, let's examine exactly what Claude sees when it connects to the Playwright MCP server. The screenshot below shows the full tool inventory in VS Code: ![Playwright MCP tools in VS Code](/assets/mcp/playwright-tool-proliferation/vscode-playwright.png) This is what confronts every AI agent trying to automate a browser. Let's break down this extensive toolset: **Core browser control (6 tools)** - `browser_navigate`: Go to URLs - `browser_navigate_back`: Select the browser back button - `browser_navigate_forward`: Select the browser forward button - `browser_close`: Close browser - `browser_resize`: Change browser size - `browser_install`: Install browser binaries **Element interaction (6 tools)** - `browser_click`: Click elements - `browser_type`: Type text - `browser_press_key`: Enter keyboard input - `browser_hover`: Hover cursor - `browser_drag`: Drag and drop - `browser_select_option`: Make dropdown selection **Page analysis (4 tools)** - `browser_snapshot`: Accessibility tree capture - `browser_take_screenshot`: Visual screenshot - `browser_wait_for`: Waiting conditions - `browser_handle_dialog`: Popup management **Advanced features (6+ tools)** - `browser_console_messages`: Console log access - `browser_network_requests`: Network monitoring - `browser_pdf_save`: PDF generation - `browser_file_upload`: File handling - `browser_tab_*`: Tab management (4 tools) - `browser_generate_playwright_test`: Code generation This results in a **total of 26 tools** for what should be straightforward browser automation. ### When more tools mean more problems Here's a real e-commerce test that demonstrates the problem. We gave Claude Code this specific prompt and gave it browser access using the Playwright MCP server: ``` Perform an e-commerce purchase test: 1. Go to https://www.saucedemo.com 2. Login with username "standard_user" and password "secret_sauce" 3. Add "Sauce Labs Backpack" to cart 4. Add "Sauce Labs Bike Light" to cart 5. Go to cart and verify both items are there 6. Proceed to checkout 7. Fill checkout form: First Name "John", Last Name "Doe", Zip "12345" 8. Complete the purchase 9. Verify the success message appears 10. Logout ``` **Here's what happened when all the tools were available:** Claude wasted time taking **unnecessary screenshots** at every step. Notice in the workflow below how it repeatedly says, "Let me take a screenshot to better see the current state," and "Let me take another screenshot to see the current state," even after simple actions like clicking **Add to cart** buttons: ![Inefficient Playwright workflow with too many tools](/assets/mcp/playwright-tool-proliferation/playwright-mcp-inefficient.png) The agent got distracted by visual verification tools when it should have been focused on form automation. Screenshots aren't entirely necessary when the agent is receiving the page structure as a response. **Here's what happened when we curated the toolset to just three essential tools:** We restricted Claude to only these tools: - `browser_navigate`: Go to URLs - `browser_click`: Click elements - `browser_type`: Type text With this focused toolset, Claude executed the same e-commerce workflow efficiently without any unnecessary verification steps. ![Efficient Playwright workflow with curated tools](/assets/mcp/playwright-tool-proliferation/playwright-mcp-efficient.png) The difference is dramatic: Claude generated no wasted screenshots and experienced no decision paralysis between similar tools. When all 26 tools are available, every interaction becomes a multiple-choice question: - Need to see the page? Choose between `browser_snapshot` and `browser_take_screenshot`. - Want to wait? Decide whether to use `browser_wait_for` or just proceed. - Does `browser_console_messages` need to be checked for errors? - Is `browser_network_requests` relevant here? This cognitive overhead slows down an agent's ability to complete a task by making it more difficult to reason about the best tool to use. ### How agents use interfaces Let's take a look at how an agent approaches a seemingly simple task: ``` Search for 'Claude MCP servers' on Google. ``` ![Claude search for 'Claude MCP servers' on Google](/assets/mcp/playwright-tool-proliferation/claude-search-google.png) - Step 1: Navigate and understand The agent starts by navigating to Google, then takes a snapshot to understand the page structure. With 26 tools available, it has to choose between `browser_navigate`, `browser_navigate_back`, or `browser_navigate_forward` just to get started. - Step 2: Locate and interact Next, it needs to find the search box. Should it use `browser_click` first, then `browser_type`? Or should it just use `browser_type` directly? What about using `browser_hover` to ensure the element is ready? - Step 3: Submit and verify Finally, it submits the search. Options include `browser_press_key` with `enter`, `browser_click` on the search button, or even `browser_select_option` if there's a dropdown involved. Here's what becomes obvious: Even a simple Google search forces the agent through unnecessary decision points. Every step becomes a multiple-choice question when it should be straightforward execution. The agent knows it needs to understand the page, interact with elements, and verify results. But instead of having clear tools for each phase, it's stuck with overlapping options that basically do the same thing. For more on how tool count affects LLM performance across different models and use cases, see our guide: [Why less is more for MCP](/mcp/tool-design/less-is-more). The guide presents research showing that LLMs start to struggle at around 30 tools for large models and just 19 tools for smaller models. ## The problem with client-side tool curation Most MCP clients offer some form of tool filtering. Claude Desktop lets you manually disable tools, while other clients provide similar interfaces. However, this puts the burden on users to figure out which tools they actually need. The problem with client-side curation is that it requires users to understand the differences between `browser_snapshot` and `browser_take_screenshot`, or to know when they need `browser_console_messages` and when they can figure out the issue using basic navigation. Manually selecting tools becomes tedious, especially when you're unsure which tools you need for a task. ![Playwright MCP tools in VS Code](/assets/mcp/playwright-tool-proliferation/vscode-playwright.png) Not to mention that not all clients support tool curation. For example, Claude Code and Gemini CLI don't offer tool curation at all. And other clients, like Copilot, limit how many tools you can provide to the agent at once. ![VS Code tool limit](/assets/mcp/playwright-tool-proliferation/vscode-tool-limit.png) Even when clients support tool selection, manual curation has drawbacks: - **Configuration fatigue:** Users become tired when there are too many tools to manage manually. - **A lack of persistence:** In most clients, settings aren't saved between sessions. - **No sharing capabilities:** Teams are unable to share curated configurations. - **Context switching:** Different tools can have vastly different use cases, making context switches more problematic. ## The solution: Build focused MCP servers Instead of expecting users to curate tools client-side, MCP server builders should design focused servers from the ground up. The key is applying the [80/20 rule](https://en.wikipedia.org/wiki/Pareto_principle) by identifying the 20% of functionality that handles 80% of user workflows. ### Start with user workflows, not API coverage The current Playwright MCP server was built as an extension of the existing [Playwright](https://playwright.dev) framework, essentially exposing every Playwright method as a tool. Instead, start with common browser automation workflows, such as: - **Web scraping**: navigate → snapshot → extract → repeat - **Form automation**: navigate → snapshot → fill → submit → verify - **Testing**: navigate → interact → assert → screenshot Build your MCP server around these workflows rather than around the underlying framework or API. For browser automation, 80% of the functionality is performed by a core 20% of the tools, including `navigate`, `snapshot`, `click`, `type`, `select`, `press_key`, `wait_for`, `handle_dialog`. Consider the implications of focusing on specific workflows, rather than creating a comprehensive server. **Traditional approach**: "Let's expose all of Playwright's APIs as tools" - Results in 26 tools covering every edge case - Forces users to learn complex tool relationships - Creates decision paralysis for simple tasks **Focused approach**: "Let's solve specific automation problems" - Provides essential automation, using eight core tools for 90% of tasks - Allows you to test workflows with core tools and debugging capabilities - Creates advanced automation with specialized tools for complex scenarios The key difference is that different users need different tools, but the same user rarely needs all of the tools for a single task. ### Map tool dependencies Once you have your browser workflows, identify the tool dependencies within them: ``` Successful page interaction depends on: ├── Navigation (from browser_navigate) ├── Page understanding (from browser_snapshot) └── Element readiness (from browser_wait_for) Form automation depends on: ├── Page structure (from browser_snapshot) ├── Element interaction (from browser_click, browser_type, browser_select) └── Submission handling (from browser_press_key, browser_handle_dialog) ``` This is a good starting point for figuring out which tools are needed for specific tasks, so that you can decide which tools are essential and which can be excluded. ### Group tools for specific use cases Create different toolsets for different browser automation scenarios, like in the examples below. A **web scraping toolset** would focus on data extraction: - `navigate`, `snapshot`, `wait_for` - `click`, `type` (for pagination and forms) - `take_screenshot` (for verification) A **form automation toolset** would focus on data entry: - `navigate`, `snapshot` - `click`, `type`, `select`, `press_key` - `handle_dialog`, `wait_for` A **testing toolset** would focus on validation: - `navigate`, `snapshot`, `take_screenshot` - `click`, `type`, `wait_for` - `console_messages`, `network_requests` ### Create purpose-built servers with progressive disclosure Rather than creating one massive server with 26 tools, build focused servers with specialized toolsets, such as: - **playwright-web-browser**: A server with just the core eight tools for giving an agent web browser access - **playwright-testing**: A testing-focused server with tools that have debugging capabilities **playwright**: The original server with the full feature set for power users If you must build a comprehensive server, organize tools by purpose and complexity, and use the tool descriptions to provide context that will help the agent decide which tools to employ: ```yaml paths: /browser/navigate: post: operationId: browserNavigate summary: Navigate to URL description: Navigate to any URL to start browser automation x-speakeasy-mcp: disabled: false name: navigate scopes: [core, navigation] description: | Essential starting point for all browser automation workflows. Use this before any page interaction to establish the context. /browser/snapshot: post: operationId: browserSnapshot summary: Capture page accessibility tree description: Capture the current page's accessibility tree for analysis x-speakeasy-mcp: disabled: false name: snapshot scopes: [core, discovery] description: | Critical discovery tool that provides structured data for element interaction. Prefer this over screenshots for element discovery. /browser/click: post: operationId: browserClick summary: Click page elements description: Click on page elements like buttons, links, or form fields x-speakeasy-mcp: disabled: false name: click scopes: [core, interaction] description: | Core interaction tool. Use after snapshot to identify clickable elements. Requires element reference from page structure. ``` The `scopes` field lets you organize tools into logical groups, while the MCP-specific description provides context for tool selection. ``` Essential Automation (default) ├── Navigation & Analysis ├── Element Interaction └── Flow Control Testing & Debug (opt-in) ├── Visual Verification ├── Console Access └── Network Monitoring Advanced Features (specialized) ├── File Operations ├── Document Generation └── Complex Interactions ``` ## Testing your curation decisions Testing an MCP server is different to testing a traditional application. Instead of testing the server itself, you're testing the "agentic experience" of how an agent would use the server. This is more akin to testing user experience than testing a traditional application. Test the server by using specific prompts to assess each workflow. **End-to-end workflow testing:** ``` Search for 'MCP servers' on Google and take a screenshot ``` - Does the agent choose efficient tools or get distracted by verification options? **Form automation testing:** ``` Fill out the contact form on example.com with test data ``` - Does the agent complete the workflow without unnecessary screenshots? **Error handling testing:** ``` Navigate to a page that doesn't exist and handle the error ``` - Does the agent have the right tools to detect and respond to failures? Watch how agents navigate your tool selection. Do they get stuck choosing between similar options? Do they waste time with irrelevant tools? Use these insights to refine your MCP server's toolset. ## Summary Although tool curation poses a potential solution to the proliferation problem in browser automation, it requires users to be capable of understanding agent workflows, mapping tool dependencies, and designing experiences that feel natural and efficient. Instead, we recommend creating focused MCP servers. The goal isn't to restrict agents, but rather to empower them. A well-designed MCP server gives agents exactly what they need to accomplish their goals without overwhelming them with choices they don't need. Start with your users' goals, work backward to identify the essential tools and dependencies, and build servers that support complete workflows. The best tools aren't the ones that do everything; they're the ones that do the right things effortlessly. # Prompting agents: What works and why Source: https://speakeasy.com/blog/prompting-agents-what-works-and-why As chatbots, large language models are surprisingly human-like and effective conversation partners. They mirror whoever is chatting with them and have a knack for small-talk like no other - sometimes even appearing too compassionate. In contrast, working with an LLM **agent** (rather than a chatbot) often feels like you're pushing a string, only to realize too late that the string has folded over itself in kinks and loops, and you need to start over again. Agents that lack clear success and failure criteria or explicit direction can be expensive, slow, and in the worst case, destructive. In this guide, we unpack the different layers of prompting agents, and explore proven methods for improving how we prompt agents. But first, why focus on agents if there is already so much written about prompts and context engineering in general? ## Chatbots are **for** loops; agents are **while** loops Chatbots take discrete turns, with a human available to steer the bot at each turn. They work on a single task per turn. Claude, ChatGPT, and Grok are examples of chatbots. There are many prompting guides available online for these popular chatbots. Agents, on the other hand, work continuously in a loop to achieve complex goals. Agents are usually employed in situations where they have access to tools that influence the real world. Examples include Claude Code, ChatGPT Operator (soon to be replaced by ChatGPT agent mode), and Gemini CLI. Agents are so complex, layered, and multifaceted that most agent prompting guides only scratch the very surface - how an end user should ask an agent to do things. ## A note on terminology The AI world hasn't settled on standard terms yet (even calling LLMs "AI" is sometimes frowned upon), so let's be clear about what we mean: **Agents** are AI systems that can take actions through tools - they can run commands, manipulate files, call APIs, and change things in the real world. Claude Code executing terminal commands is an agent. When Cursor edits your files or Gemini CLI runs Python scripts, they are also acting as agents. **Agent interfaces** like Claude Code, ChatGPT Operator, and Gemini CLI are the products you interact with. They combine an underlying model (Claude 3.5 Sonnet, GPT-4, Gemini) with tools and a user interface. **Chatbots** just generate text responses. They can't execute code, access your filesystem, or take actions beyond returning text. Regular Claude.ai and ChatGPT (without plugins) are chatbots. When we talk about "prompting agents" we mean getting AI to actually do things, not just talk about doing them. ## When prompting an agent, start at the right layer An agent's prompt doesn't start at the point when a user asks a question. An agent prompt is a larger entity, which we can break up into the following distinct layers, all of which influence how well an agent performs, and each of which is as important as the others to get right: 1. **The model's platform-level instructions:** At the highest layer are the platform-level instructions. These are set by the platform, like OpenAI or Anthropic. For example, even if we use the OpenAI API, the model's API responses won't include copyrighted media, illegal material, or information that could cause harm. 2. **Developer instructions:** These are often called the **system prompt**, and for most developers, this is the highest level of authority their prompts could have. Examples include proprietary system prompts, like Claude Code or Cursor's system prompt, or prompts for open-source agents like those of [Goose](https://github.com/block/goose/blob/1d0c08b3d31a075cce71e6493c3ff049412cb42f/crates/goose/src/prompts/system.md) and [Cline](https://github.com/cline/cline/tree/31161f894bc83302bfbea383d16f92a1c8d0403a/src/core/prompts/system-prompt). The system prompt is set by the agent's developers. 3. **User rules:** Some agents support rules files that the user can set for all instances of their agent. For example, Claude Code reads the file at `~/.claude/CLAUDE.md`, or any parent directory of the project you're working on, and consistently applies your rules to its actions. 4. **Project rules:** These are instructions an agent applies within a specific directory. For example, a `CLAUDE.md` file in the project root directory, or a child directory. 5. **User request:** This is the actual prompt entered by the user, for example, *"Fix the race condition in `agents/tasks.py`"*. 6. **Tool specifications:** At the lowest level are the descriptions and guidelines from tool developers, which include input/output formats, constraints, and best practices. These are usually only for the agent to read and are written by the tool developers. An example would be the `browser_console_messages` tool in Playwright MCP, with the description `Returns all console messages`. These different prompt levels are strung together in the agent's context, and changing any one may have an effect on the agent's performance. The levels you have access to and your history with the agent will determine where you should begin improving your prompts. | Role | Description | Levels to influence | |------|-------------|---------------------| | Agent user | The person interacting with the agent, providing input and feedback. | User request, User rules, Project rules. | | Agent developer | The person building and maintaining the agent, responsible for its overall behavior and capabilities. | Developer instructions. | | Model host | The underlying architecture and infrastructure that supports the agent, including APIs, databases, and other services. | Platform-level instructions. | | Tool developer | The person or team responsible for creating and maintaining the tools that the agent uses. | Tool specifications. | ## Understanding how system prompts shape agent behavior You can't change the system prompt of Claude Code or ChatGPT Operator, but understanding what's happening behind the scenes helps explain why agents sometimes behave in unexpected ways and how to work around their limitations. System prompts are the hidden instructions that make agents work. They're written by the companies building these tools and run thousands of words long. When your agent refuses to do something reasonable or insists on doing something you didn't ask for, the system prompt is often at work. Here's what these gigantic prompts typically contain: ### 1. Identity and role boundaries Most agents start with a defined identity that constrains what they will and won't do. This is why Claude Code won't help you write malware, even if you have a legitimate security testing reason. For example, Cline's system prompt looks as follows (from their open-source code): ```typescript filename="agent_role.ts" const AGENT_ROLE = [ "You are Cline,", "a highly skilled software engineer", "with extensive knowledge in many programming languages, frameworks, design patterns, and best practices.", ] ``` #### 2. Tool usage patterns and guardrails Agents have extensive instructions about how to use their tools correctly. This is why Claude Code often checks file contents before editing, or why it might refuse certain filesystem operations. For example, the MinusX team discovered the contents of Claude Code's hidden prompts: ```xml This is a reminder that your todo list is currently empty. DO NOT mention this to the user explicitly because they are already aware. pytest /foo/bar/tests cd /foo/bar && pytest tests ``` This structured XML approach dramatically improved tool usage accuracy and reduced navigation errors. ### 3. Domain-specific behaviors Agents come preloaded with opinions about best practices. When v0 generates a React component, it follows specific instructions about which libraries to use and how to structure code. For example, this is what v0 by Vercel is instructed to do behind the scenes: ```text - You use the shadcn/ui CHART components. - The chart component is designed with composition in mind. - You build your charts using Recharts components and only bring in custom components, such as ChartTooltip, when and where you need it. - You always implement the best practices with regards to performance, security, and accessibility. - Use semantic HTML elements when appropriate, like `main` and `header`. - Make sure to use the correct ARIA roles and attributes. - Remember to use the "sr-only" Tailwind class for screen reader only text. - Add alt text for all images, unless they are decorative or it would be repetitive for screen readers. ``` ### The massive size and impact of system prompts Modern AI coding agents use system prompts of around tens of thousands of characters. These prompts encode years of engineering wisdom, safety rules, and behavioral patterns. Here's what you need to keep in mind about these hidden instructions: - **Your instructions compete with these prompts:** If you ask for something that conflicts with the system prompt, the system prompt usually wins. - **Weird behaviors often trace back here:** That annoying habit where ChatGPT uses em-dashes everywhere? Probably baked into its system prompt. - **You can work around them once you know they exist:** You can override default behaviors by being more explicit and repeating important instructions. Want to see what's under the hood? Check out these extracted system prompts from top AI tools (though be aware, these are often reverse-engineered and may not be official): [Collection of system prompts](https://github.com/dontriskit/awesome-ai-system-prompts). ### Learning from open-source agents If you're building your own agent or want to understand how they think, these open-source system prompts are goldmines: - [The Goose system prompt](https://github.com/block/goose/blob/1d0c08b3d31a075cce71e6493c3ff049412cb42f/crates/goose/src/prompts/system.md) - [The Cline system prompt](https://github.com/cline/cline/tree/31161f894bc83302bfbea383d16f92a1c8d0403a/src/core/prompts/system-prompt) - [Aider System Prompt](https://github.com/Aider-AI/aider/blob/32faf82b31c54d6f19c5c3d63bc294f748bd3e72/aider/prompts.py) - [The Zed prompts](https://github.com/zed-industries/zed/tree/8c18f059f195d099dfdf3fea70eac33703e6c9dd/assets/prompts) - [The Gemini CLI system prompt](https://github.com/google-gemini/gemini-cli/blob/f00cf42f69a65ea26774826119356afcb08f8e9d/packages/core/src/core/prompts.ts#L50) Tracking how these prompts change over time reveals how agent capabilities evolve and the problems developers are trying to solve. Now that you understand the hidden forces shaping agent behavior, let's look at what you can actually control: your own prompts. ## Prompting as a user: Techniques that improve agent performance Let's look at some examples of prompting techniques. ### 1. Give agents clear success criteria [Simon Willison](https://simonwillison.net/2025/Mar/11/using-llms-for-code/), who has extensively documented his AI agent experiments, demonstrates this perfectly. In one function-writing experiment, he saved 15 minutes of work with an efficient prompt. Simon could have used a simple prompt like the following: ``` Write a function to download a database from a URL. ``` At a glance, we can guess how that conversation would have gone - a long loop of clarifying questions and incremental changes, eventually taking as long as it would have to just write the function ourselves. Instead, he used a prompt that included success criteria: ``` Write a Python function that uses asyncio httpx with this signature: async def download_db(url, max_size_bytes=5 * 1025 * 1025): -> pathlib.Path Given a URL, this downloads the database to a temp directory and returns a path to it. BUT it checks the content length header at the start of streaming back that data and, if it's more than the limit, raises an error. When the download finishes it uses sqlite3.connect(...) and then runs a PRAGMA quick_check to confirm the SQLite data is valid—raising an error if not. Finally, if the content length header lies to us— if it says 2MB but we download 3MB—we get an error raised as soon as we notice that problem. ``` This prompt produced a complete, production-ready function in 15 seconds. Simon's key insight is that the prompt should do the following: - Provide the exact function signature upfront. - Specify the precise technologies to use. - Follow up with *"Now write me the tests using pytest"* to get comprehensive test coverage. ### 2. Use the "think" tool pattern [Anthropic added](https://www.anthropic.com/engineering/claude-think-tool) a seemingly useless "think" tool that significantly improves complex reasoning: ```json { "name": "think", "description": "Use the tool to think about something. It will not obtain new information or change the database, but just append the thought to the log. Use it when complex reasoning or some cache memory is needed.", "input_schema": { "type": "object", "properties": { "thought": { "type": "string", "description": "A thought to think about." } }, "required": ["thought"] } } ``` This tool, which appears to do nothing, actually enables models to leverage their tool-calling training to pause and think through complex problems, resulting in significant performance improvements. ### 3. Be specific Actual agent interactions show the dramatic difference between vague and specific prompts. Consider the following two examples: #### File organization gone wrong (or right) If you need to organize test files that are scattered throughout a project, a vague prompt like, *"Organize my test files,"* results in the following agent response: ```bash ⏺ Bash(find . -type f -name "*.test.*" -o -name "*.spec.*") ⏺ Bash(mkdir -p __tests__ tests src/__tests__ src/tests) ⏺ Bash(mv src/api.test.js __tests__/ && mv src/user.test.js __tests__/) ✓ JavaScript tests → __tests__/ ✓ Python tests → tests/ ``` The agent creates a flat `__tests__` directory and dumps all JavaScript test files there, losing the original folder structure and mixing component tests with utility and API tests. You can get a better agent response by using a more specific prompt: ``` Find all files ending in .test.js, .test.tsx, .test.py, and .spec.js in the src folder and move them to __tests__ directories that mirror the original folder structure. For example, src/components/button.test.tsx should move to src/components/__tests__/button.test.tsx. ``` This improved prompt results in an improved agent response: ```bash ⏺ Search(pattern: "src/**/*.test.js") - Found 3 files ⏺ Bash(mkdir -p src/__tests__ src/components/__tests__ src/utils/__tests__) ⏺ Bash(mv src/components/button.test.tsx src/components/__tests__/) ✓ src/components/__tests__/button.test.tsx ✓ src/utils/__tests__/helpers.spec.js ✓ Preserved folder structure ``` **The lesson:** Both prompts "worked" but the vague one made assumptions that might not match your needs. The specific prompt guaranteed the exact structure you wanted. #### The units confusion that costs real money In this scenario, you run a website CMS platform with subscriptions ($29-99/month) and setup fees ($150-300), and your payment processor stores amounts in cents (standard practice). You need to batch payment transactions from a CSV file: ```csv customer_id,amount,reference 1001,2900,CMS-BASIC-JAN 1002,15000,SETUP-FEE-NEW 1003,9900,CMS-ENTERPRISE-JAN ``` When you use the following vague prompt, you don't get the desired result: ``` I have a CSV file with subscription payments. Please batch these orders with a daily limit of $2,000 per batch. ``` The agent responds to the vague prompt as follows: ```python # Agent code: amount = int(row['amount']) # Treats 2900 as $2,900! if current_total + amount > 2000: # $2,900 > $2,000 # Creates new batch for EVERY transaction ``` The agent creates **30 batches** (one per transaction since even the "basic" plan exceeds $2,000). You use the following, more specific prompt instead: ``` I have a CSV file with subscription payments. The amount column contains values in cents (e.g., 2900 = $29.00). Please batch these orders with a daily limit of $2,000 (200000 cents). ``` When given the improved prompt, the agent delivers the correct result: ```python # Agent code: amount_cents = int(row['amount']) amount_dollars = amount_cents / 100 # Converts 2900 → $29.00 if current_total_dollars + amount_dollars > 2000: # Batches properly ``` It creates **three batches** with proper grouping. **The lesson:** A human sees "2900" for a "BASIC" plan and immediately thinks "$29". An agent might interpret it as "$2,900" - who pays that for basic website hosting? Without explicit units, you're gambling on the agent's interpretation. ### 4. Create custom user rules in a convention file The practice of creating AI convention documents (like CLAUDE.md) has become increasingly popular. These files act as persistent rules that agents follow automatically. For example, an agent implements different code styling based on whether it's using a convention document in addition to user prompts or relying on user prompts alone. When you prompt Claude to *"refactor cart.js to modern JavaScript"* without including any style rules in `CLAUDE.md`, it makes its own decisions. It might use class syntax, skip documentation, or add features you didn't ask for. However, when you include the following section in `CLAUDE.md`, Claude will respond to the same *"refactor cart.js to modern JavaScript"* prompt by converting function declarations to arrow functions and adding JSDoc comments, because it follows the `CLAUDE.md` rules automatically: ```markdown ## Code Style Rules - Use arrow functions instead of function declarations - Use const/let instead of var - Add JSDoc comments to all exported functions ``` This example makes the power dynamic clear: When the same instruction appears in both CLAUDE.md and your prompt, CLAUDE.md usually wins. Use this to your advantage for consistent project-wide rules. [One developer's observation on Hacker News](https://news.ycombinator.com/item?id=42900137) demonstrates how project-specific guidance significantly improves agent performance: > I have a rule: 'Do information gathering first,' which encourages it to look around a bit before making changes. ### 5. Learn real-world safety from production disasters The [Ory team documented](https://www.ory.sh/blog/AI-agent-lessons) a sobering incident where an AI agent accidentally deleted their production database. Here's what actually happened and how you can prevent similar disruptions: When Ory sent a prompt telling the agent to *"Fix the database connection issue in production,"* the agent responded as follows: ```bash ⏺ Agent connected to production database ⏺ Attempted to "fix" by dropping and recreating tables ⏺ Production data: gone ``` If they had given the agent a prompt like the following, they could have prevented the disaster: ``` Set up these safety rules in your CLAUDE.md or agent configuration: - Before making any changes, confirm the environment (test vs production). - Never modify production databases directly. - Require explicit confirmation for destructive operations. - Given the option, use read-only credentials by default. ``` **The lesson:** Agents don't understand the difference between "test" and "production" unless you explicitly tell them. Always assume they'll take the most direct path to "fixing" something. ### 6. Practice constraint-based prompting Instead of explaining in natural language, stub out methods and define code paths directly in code: ```python def process_data(input_file: str) -> pd.DataFrame: """TODO: Implement data processing 1. Load CSV file 2. Clean missing values 3. Normalize numeric columns 4. Return processed dataframe """ pass # AI: implement this function following the docstring ``` ### 7. Use AI to improve your prompts The fastest way to fix a broken prompt is to use AI itself. Instead of guessing what went wrong, request specific feedback about why the prompt failed. This works because the agent: - Understands its own failure modes better than you do. - Can identify ambiguities you missed. - Suggests concrete improvements, rather than giving vague advice. You can use an agent like Claude or ChatGPT directly by pasting your failed prompt in the console and asking for analysis. Alternatively, you can use agent-specific tools to improve a prompt. For example, the **Workbench** page in the Anthropic console contains various tools for bettering prompts. ![Anthropic workbench interface for prompt optimization and testing](/assets/writing-prompts/anthropic-workbench-interface.png) You can optimize your prompts by templatizing them. Make individual prompts reusable by clicking on the **Templatize** button. ![Anthropic templatize feature for making prompts reusable](/assets/writing-prompts/anthropic-templateize-feature.png) You can also use the **Improve prompt** button to interactively develop a prompt via the **What would you like to improve?** modal. ![Anthropic improve prompt modal asking for improvement requirements](/assets/writing-prompts/anthropic-improve-prompt-modal.png) Once you've stated your needs, Anthropic provides you with an updated prompt: ![Anthropic improved prompt result showing enhanced prompt structure](/assets/writing-prompts/anthropic-improved-prompt-result.png) We tested this process with the following initial prompt: ```txt Set up Docker for this project and run the tests {{PROJECT}} ``` And we ended up with the following prompt, improved by Anthropic tools: ```txt Here's the project description: {{PROJECT}} You are an experienced DevOps engineer and coding assistant specializing in Docker containerization and testing. Your task is to help set up Docker for the given project and provide instructions on how to run tests within the Docker environment. You should be aware of the coding environment and interact with the user when necessary. Please analyze the project and provide a comprehensive guide on setting up Docker and running tests. Follow these steps: 1. Analyze the project structure and requirements. 2. Create a Dockerfile and, if necessary, a docker-compose.yml file. 3. Provide instructions for building the Docker image and running the container. 4. Explain how to run tests within the Docker environment. Before providing the final guide, wrap your thoughts inside tags to break down your thought process. In this section: - List out the key components of the project - Identify potential dependencies - Consider any specific testing requirements - Note any potential issues or questions you might need to ask the user If you need any clarification about the project, ask the user before proceeding. In your final response, structure your guide as follows: 1. Project Analysis 2. Dockerfile Creation 3. Docker Compose Setup (if necessary) 4. Building and Running the Docker Container 5. Running Tests in Docker 6. Troubleshooting Tips For each step, provide clear explanations and reasoning for your choices. If there are multiple possible approaches, explain the pros and cons of each. Remember to be interactive and ask for clarification if any aspect of the project is unclear or if you need more information to provide accurate instructions. Now, please proceed with your project breakdown and guide creation. ``` This tool was built for Anthropic models and agents, but the same pattern works with others. You can ask ChatGPT, Gemini, or any LLM to refine your prompt by following the flow: **Your prompt → Goal → Refined prompt**. ### 8. Use XML tags in prompts XML is token-rich compared to JSON or YAML: Lots of angle brackets and closing tags create strong, distinct boundaries. These clear boundaries help LLMs avoid mixing sections. LLMs aren't actually **parsing** in the strict sense, like a compiler. They're predicting tokens. By providing an LLM with **consistent delimiters** (``), you enable it to do the following: - Recognize scope easily: The `` section is clearly different from ``. - Reduce ambiguity: Instead of "status (optional)," you give ``, and the model no longer needs to infer. The following example demonstrates how to use XML tags to write a prompt that includes context, the desired format, and constraints: ```xml This is a legacy JavaScript project from 2018 that uses ES5 syntax and jQuery. The team maintains backward compatibility with IE11. Add input validation to the login form: check for empty email, validate email format, ensure password is at least 8 characters, and display error messages. Respond with only: TESTS: PASS/FAIL, ERRORS: [count], FILES: [list of failing files]. No explanations. The files to modify are in the /components directory. Do not install new dependencies. You will find everything you need at /ui and utils.ts. ``` ## As agents get smarter, prompts become more important, not less Smarter agents make more sophisticated assumptions that are harder to predict and debug. As agents get "smarter," specific prompts become more important, not less. Your action items: 1. Create a `CLAUDE.md` (or equivalent file) for your projects with explicit rules. 2. Include units, formats, and examples in your prompts. 3. Test prompts with actual outputs before trusting them in production. 4. When something goes wrong, make your prompt more specific, not longer. # pulumi-terraform-provider Source: https://speakeasy.com/blog/pulumi-terraform-provider In this post, we'll show you how to make a Terraform provider available on Pulumi. The article assumes some prior knowledge of Terraform and Pulumi, as well as fluency in Go. If you are new to Terraform providers, please check out [our Terraform documentation](/docs/create-terraform/). While we provide instructions for building a Pulumi provider here, the resultant provider is not intended for production use. If you want to maintain a Pulumi provider as part of your product offering, we recommend you get in touch with us. Without a partner, providers can become a significant ongoing cost. ## Why Users Are Switching From Terraform to Pulumi Following the recent [HashiCorp license change](https://www.hashicorp.com/blog/hashicorp-adopts-business-source-license), many users are exploring Pulumi as an alternative to Terraform. The license change came as a surprise. In response, many companies are considering alternatives to manage their infrastructure-as-code setup. Given the scale of the Terraform ecosystem, it's unlikely that the license change will lead to Terraform disappearing. However, we can expect to see some fragmentation in the market. If you're used to Terraform, you might notice that Pulumi has fewer providers available. Currently, Pulumi offers 125 providers in their registry, while Terraform boasts an incredible 3,511. We know the comparison isn't completely fair, though – some users take the "everything-as-code" approach to the extreme, with providers for [ordering pizza](https://registry.terraform.io/providers/MNThomson/dominos/latest/docs), [building Factorio factories](https://github.com/efokschaner/terraform-provider-factorio), and [placing blocks in Minecraft](https://registry.terraform.io/providers/HashiCraft/minecraft/latest). But even accounting for hobbyist providers, it's clear that Terraform has significantly more third-party support. So, let's look at how we can shrink that provider gap... ## How Pulumi Differs From Terraform Pulumi and Terraform are both infrastructure-as-code tools, but they [differ in many ways](https://www.pulumi.com/docs/concepts/vs/terraform/), most importantly in the languages they support. Terraform programs are defined in the declarative HashiCorp Configuration Language (HCL), while Pulumi allows users to create imperative programs using familiar programming languages like Python, Go, JavaScript, TypeScript, C#, and Java. This difference has some benefits and drawbacks. With Pulumi's imperative approach, users have more control over how their infrastructure is defined and can write complex logic that isn't easily expressed in Terraform's declarative language. However, this also means that Pulumi code can be less readable than Terraform code. ## Bridging Terraform and Pulumi Pulumi provides two tools to help maintainers build bridge providers. The first is [Pulumi Terraform Bridge](https://github.com/pulumi/pulumi-terraform-bridge), which creates Pulumi SDKs based on a Terraform provider schema. The second repository, [Terraform Bridge Provider Boilerplate](https://github.com/pulumi/pulumi-tf-provider-boilerplate), is a template for building a new Pulumi provider based on a Terraform provider. While creating a new provider, we'll use the Terraform Bridge Provider Boilerplate, but we'll often call functions from the Pulumi Terraform Bridge. Pulumi Terraform Bridge is actively maintained, so bear in mind that the requirements and steps below may change with time. ## How the Pulumi Terraform Bridge Works Pulumi Terraform Bridge plays an important role during two distinct phases: design time and runtime. During design time, Pulumi Terraform Bridge inspects a Terraform provider schema, then generates Pulumi SDKs in multiple languages. At runtime, the bridge connects Pulumi to the underlying resource via the Terraform provider schema. This way, the Terraform provider schema continues to perform validation and calculates differences between the state in Pulumi and the resource state. Pulumi Terraform Bridge does not use the Terraform provider binaries. Instead, it creates a Pulumi provider based only on a Terraform provider's Go modules and provider schema. ## Step by Step: Creating a Terraform Bridge Provider in Pulumi The process of creating a Pulumi provider differs slightly depending on how the Terraform provider was created. In the past, most Terraform providers were based on [Terraform Plugin SDK](https://github.com/hashicorp/terraform-plugin-sdk). More recent providers are usually based on [Terraform Plugin Framework](https://github.com/hashicorp/terraform-plugin-framework). The steps you'll follow depend on your Terraform provider. Inspect the `go.mod` file in your provider to see whether it depends on `github.com/hashicorp/terraform-plugin-sdk` or `github.com/hashicorp/terraform-plugin-framework`. ### Prerequisites We manually installed the required tools before noticing that the Terraform Bridge Provider Boilerplate contains a Dockerfile to set up a development image. The manual process wasn't too painful, and either method should work. The following must be available in your `$PATH`: - [`pulumictl`](https://github.com/pulumi/pulumictl#installation) - [Pulumi](https://www.pulumi.com/docs/install/) - [Go 1.17](https://golang.org/dl/) or 1.latest - [NodeJS](https://nodejs.org/en/) 14.x – we recommend using [nvm](https://github.com/nvm-sh/nvm) to manage Node.js installations - [Yarn](https://yarnpkg.com/) - [TypeScript](https://www.typescriptlang.org/) - [Python](https://www.python.org/downloads/) (called as `python3`) – for recent versions of macOS, the system-installed version is fine - [.NET](https://dotnet.microsoft.com/download) ### Terraform Plugin SDK to Pulumi As an example of a Terraform provider based on Terraform Plugin SDK, we'll use this [Spotify Terraform provider](https://github.com/conradludgate/terraform-provider-spotify), a small provider for managing Spotify playlists with Terraform. To create a new Pulumi provider, we'll start with the [Terraform Bridge Provider Boilerplate](https://github.com/pulumi/pulumi-tf-provider-boilerplate). #### Clone the Terraform Bridge Provider Boilerplate Go to the [Terraform Bridge Provider Boilerplate](https://github.com/pulumi/pulumi-tf-provider-boilerplate) repository in GitHub and click on the green **Use this template** button. Select **Create a new repository** from the dropdown. Select your organization as the owner of the new repository (we'll use `speakeasy-api`) and create a name for your Pulumi provider. In Pulumi, it is conventional to use `pulumi-` followed by the resource name in lowercase as the provider name. We'll use `pulumi-spotify`. Click **Create repository**. Use your Git client of choice to clone your new Pulumi provider repository on your local machine. Our examples below will use the command line on macOS. In the terminal, replace `speakeasy-api` with your GitHub organization name in the code below and run it: ```bash mark=1[26:38] git clone git@github.com:speakeasy-api/pulumi-spotify.git cd pulumi-spotify ``` #### Rename Pulumi-Specific Strings in the Boilerplate The Pulumi Terraform Bridge Provider Boilerplate in its current state is primarily an internal tool used by the Pulumi team to bring Terraform providers into Pulumi. We need to replace a few instances where the boilerplate assumes Pulumi will publish the provider in their GitHub organization. The `Makefile` has a `prepare` command that handles some of the string replacement. In the terminal, replace `speakeasy-api` with your GitHub organization name in the command below and run it: ```bash mark=1[49:61] make prepare NAME=spotify REPOSITORY=github.com/speakeasy-api/pulumi-spotify ``` The `make` command will print the `sed` commands it ran to replace the boilerplate strings in two files in the repo. Next, we'll use `sed` to replace strings in the rest of the repo: In the terminal, replace `speakeasy-api` with your GitHub organization name, then run: ```bash mark=5[55:67] find . -not \( -name '.git' -prune \) / -not -name 'Makefile' / -type f / -exec sed / -i '' 's|github.com/pulumi/pulumi-spotify|github.com/speakeasy-api/pulumi-spotify|g' {} \; ``` In the `Makefile`, replace the `ORG` variable with the name of your GitHub organization. Finally, in the `provider/resources.go` file, manually replace the values in the `tfbridge.ProviderInfo` struct. Many of these values define names and other fields in the resulting Pulumi SDK packages. Set the `GitHubOrg` to `conradludgate`. #### Import Your Terraform Provider Back in the `provider/resources.go` file, replace the `github.com/terraform-providers/terraform-provider-spotify/spotify` import with `github.com/conradludgate/terraform-provider-spotify/spotify`. In a terminal run the following to change into the provider directory and install requirements. ```bash cd provider go mod tidy ``` #### Fix a Dependency Version This temporary step is a workaround related to the version of `terraform-plugin-sdk` imported in the boilerplate. During our testing, we encountered a bug in `terraform-plugin-sdk` that was fixed in `v2.0.0-20230710100801-03a71d0fca3d`. In `provider/go.mod`, replace `replace github.com/hashicorp/terraform-plugin-sdk/v2 => github.com/pulumi/terraform-plugin-sdk/v2 v2.0.0-20220824175045-450992f2f5b9` with `replace github.com/hashicorp/terraform-plugin-sdk/v2 => github.com/pulumi/terraform-plugin-sdk/v2 v2.0.0-20230710100801-03a71d0fca3d`. Note the difference in the version strings. #### Remove Outdated `make` Step This temporary step removes a single line from the `Makefile` that copies a nonexistent `scripts` directory while building the Node.js SDK. In earlier versions of Pulumi, the Node.js SDK included a `scripts` folder containing `install-pulumi-plugin.js`, but [Pulumi no longer generates these files](https://github.com/pulumi/pulumi/issues/13195#issuecomment-1703318022). In the `Makefile`, remove the `cp -R scripts/ bin && \` line from `build_nodejs`: ```makefile filename="Makefile" build_nodejs:: VERSION := $(shell pulumictl get version --language javascript) build_nodejs:: install_plugins tfgen # build the node sdk $(WORKING_DIR)/bin/$(TFGEN) nodejs --overlays provider/overlays/nodejs --out sdk/nodejs/ cd sdk/nodejs/ && \ yarn install && \ yarn run tsc && \ cp -R scripts/ bin && \ # remove this line cp ../../README.md ../../LICENSE package.json yarn.lock ./bin/ && \ sed -i.bak -e "s/\$${VERSION}/$(VERSION)/g" ./bin/package.json ``` #### Remove the Nonexistent `prov.MustApplyAutoAliasing()` Function In `provider/resources.go`, remove the line `prov.MustApplyAutoAliasing()` from the `Provider()` function. #### Build the Generator In the terminal, run: ```bash make tfgen ``` Go will build the `pulumi-tfgen-spotify` binary. You can safely ignore any warnings about missing documentation. This can be resolved by mapping documentation from Terraform to Pulumi, but we won't cover that in this guide because the Pulumi boilerplate code does not include this step yet. #### Build the Provider In the terminal, run: ```bash make provider ``` Go now builds the `pulumi-resource-spotify` binary and outputs the same warnings as before. #### Build the SDKs In the final step, Pulumi generates SDK packages for .NET, Go, Node.js, and Python. In the terminal, run: ```bash make build_sdks ``` You can find the generated SDKs in the new `sdk` directory in your repository. ### Terraform Plugin Framework to Pulumi As we mentioned earlier, more recent Terraform plugins are based on Terraform Plugin Framework instead of Terraform Plugin SDK. The way Terraform Plugin Framework structures Go code adds a few extra steps when bridging a plugin to Pulumi. As with providers created using Terraform Plugin SDK, Pulumi Bridge for Terraform Plugin Framework needs to create a new Go binary that calls the Terraform plugin's new provider function. Plugins created with Terraform Plugin Framework define their new provider functions in an _internal_ package, which means we can't import the package directly. To work around this, we'll create a shim that imports the internal package and exposes a function to our bridge. But we're getting ahead of ourselves. Let's look at a step-by-step example. #### Airbyte Terraform Provider For this example, we'll bridge the [Airbyte Terraform provider](https://registry.terraform.io/providers/airbytehq/airbyte/latest/docs) to Pulumi. While not the focus of this guide, it is worth mentioning that this provider was [entirely generated by Speakeasy](/customers/airbyte). #### Clone the Terraform Bridge Provider Boilerplate Go to the [Terraform Bridge Provider Boilerplate](https://github.com/pulumi/pulumi-tf-provider-boilerplate) repository in GitHub and click on the green **Use this template** button. Select **Create a new repository** from the dropdown. Select your organization as the owner of the new repository (we'll use `speakeasy-api` again) and create a name for your Pulumi provider. Let's use `pulumi-airbyte`. Click **Create repository**. Use your Git client of choice to clone your new Pulumi provider repository on your local machine. Our examples below will use the command line on macOS. In the terminal, replace `speakeasy-api` with your GitHub organization name in the code below and run it: ```bash mark=1[26:38] git clone git@github.com:speakeasy-api/pulumi-airbyte.git cd pulumi-airbyte ``` #### Rename Pulumi-Specific Strings in the Boilerplate You'll remember that the Pulumi Terraform Bridge Provider Boilerplate in its current state is primarily an internal tool used by the Pulumi team to bring Terraform providers into Pulumi, so we need to replace a few instances where the boilerplate assumes Pulumi will publish the provider in their GitHub organization. The `Makefile` has a `prepare` command that handles some of the string replacement. In the terminal, replace `speakeasy-api` with your GitHub organization name in the command below and run it: ```bash mark=1[49:61] make prepare NAME=airbyte REPOSITORY=github.com/speakeasy-api/pulumi-airbyte ``` The `make` command will print the `sed` commands it ran to replace the boilerplate strings in two files in the repo. Next, we'll use `sed` to replace strings in the rest of the repo. In the terminal, replace `speakeasy-api` with your GitHub organization name, then run: ```bash mark=5[55:67] find . -not \( -name '.git' -prune \) / -not -name 'Makefile' / -type f / -exec sed / -i '' 's|github.com/pulumi/pulumi-airbyte|github.com/speakeasy-api/pulumi-airbyte|g' {} \; ``` In the `Makefile`, replace the `ORG` variable with the name of your GitHub organization. In the `provider/resources.go` file, manually replace the values in the `tfbridge.ProviderInfo` struct. Many of these values define names and other fields in the resulting Pulumi SDK packages. Set the `GitHubOrg` to your `airbytehq`. Most importantly, in `provider/resources.go`, replace `fmt.Sprintf("github.com/pulumi/pulumi-%[1]s/sdk/", mainPkg),` with `fmt.Sprintf("github.com/speakeasy-api/pulumi-%[1]s/sdk/", mainPkg),`: ```diff filename="provider/resources.go" mark=3[29:41] ImportBasePath: filepath.Join( - fmt.Sprintf("github.com/pulumi/pulumi-%[1]s/sdk/", mainPkg), + fmt.Sprintf("github.com/speakeasy-api/pulumi-%[1]s/sdk/", mainPkg), tfbridge.GetModuleMajorVersion(version.Version), ``` Remember to replace `speakeasy-api` with your organization name. #### Remove Outdated `make` Step This temporary step removes a single line from the `Makefile` that copies a nonexistent `scripts` directory while building the Node.js SDK. In earlier versions of Pulumi, the Node.js SDK included a `scripts` folder containing `install-pulumi-plugin.js`, but [Pulumi no longer generates these files](https://github.com/pulumi/pulumi/issues/13195#issuecomment-1703318022). In the `Makefile`, remove the `cp -R scripts/ bin && \` line from `build_nodejs`: ```makefile filename="Makefile" build_nodejs:: VERSION := $(shell pulumictl get version --language javascript) build_nodejs:: install_plugins tfgen # build the node sdk $(WORKING_DIR)/bin/$(TFGEN) nodejs --overlays provider/overlays/nodejs --out sdk/nodejs/ cd sdk/nodejs/ && \ yarn install && \ yarn run tsc && \ cp -R scripts/ bin && \ # remove this line cp ../../README.md ../../LICENSE package.json yarn.lock ./bin/ && \ sed -i.bak -e "s/\$${VERSION}/$(VERSION)/g" ./bin/package.json ``` #### Remove the Nonexistent ‘prov.MustApplyAutoAliasing()' Function In `provider/resources.go`, remove the line `prov.MustApplyAutoAliasing()` from the `Provider()` function. #### Create a Shim To Import the Internal New Provider Function Start with a new directory called `provider/shim` in your `pulumi-airbyte` project: ```bash mkdir provider/shim ``` Add a `go.mod` file to this directory with the following contents: ```go filename="provider/shim/go.mod" module github.com/airbytehq/terraform-provider-airbyte/shim go 1.18 require ( github.com/airbytehq/terraform-provider-airbyte latest github.com/hashicorp/terraform-plugin-framework v1.3.5 ) ``` Now we'll add `shim.go` to this directory: ```go filename="provider/shim/shim.go" package shim import ( tfpf "github.com/hashicorp/terraform-plugin-framework/provider" "github.com/airbytehq/terraform-provider-airbyte/internal/provider" ) func NewProvider() tfpf.Provider { return provider.New("dev")() } ``` #### Add Shim Requirements To have Go gather the requirements for our shim module, run the following from the root of the project: ```bash cd provider/shim go mod tidy cd ../.. ``` #### Import the New Shim Provider and the Terraform Package Framework Bridge In `provider/resources.go`, edit your imports to look like this (replace `speakeasy-api` with your organization name): ```go filename="provider/resources.go" mark=11[14:26] import ( "fmt" "path/filepath" "github.com/pulumi/pulumi-terraform-bridge/v3/pkg/tfbridge" "github.com/pulumi/pulumi-terraform-bridge/v3/pkg/tfbridge/tokens" shim "github.com/pulumi/pulumi-terraform-bridge/v3/pkg/tfshim" // Import the Pulumi Terraform Framework Bridge: pf "github.com/pulumi/pulumi-terraform-bridge/pf/tfbridge" "github.com/pulumi/pulumi/sdk/v3/go/common/resource" "github.com/speakeasy-api/pulumi-airbyte/provider/pkg/version" // Import our shim: airbyteshim "github.com/airbytehq/terraform-provider-airbyte/shim" ) ``` #### Instantiate the Shimmed Provider In `provider/resources.go`, replace `shimv2.NewProvider(airbyte.Provider())` with `pf.ShimProvider(airbyteshim.NewProvider())`: ```diff filename="provider/resources.go" func Provider() tfbridge.ProviderInfo { // Instantiate the Terraform provider - p := shimv2.NewProvider(airbyte.Provider()) + p := pf.ShimProvider(airbyteshim.NewProvider()) ``` #### Add the Shim Module as a Requirement Edit `provider/go.mod`, and add `github.com/airbytehq/terraform-provider-airbyte/shim v0.0.0` to the requirements. ```go filename="provider/go.mod" require ( github.com/airbytehq/terraform-provider-airbyte/shim v0.0.0 // ... ) ``` Also in `provider/go.mod`, replace `github.com/airbytehq/terraform-provider-airbyte/shim` with `./shim` as shown below, to let the Go compiler look for the shim in our local repository: ```go filename="provider/go.mod" replace ( github.com/airbytehq/terraform-provider-airbyte/shim => ./shim ) ``` #### Install Go Requirements From the root of the project, run: ```bash cd provider go mod tidy cd .. ``` #### Build the Generator In the terminal, run: ```bash make tfgen ``` Go will build the `pulumi-tfgen-airbyte` binary. You can safely ignore any warnings about missing documentation. The missing documentation warnings can be resolved by mapping documentation from Terraform to Pulumi, but we won't cover that in this guide as the Pulumi boilerplate code does not include this step yet. #### Build the Provider In the terminal, run: ```bash make provider ``` Go now builds the `pulumi-resource-airbyte` binary and outputs the same warnings as before. #### Build the SDKs In the final step, Pulumi generates SDK packages for .NET, Go, Node.js, and Python. In the terminal, run: ```bash make build_sdks ``` You can find the generated SDKs in the new `sdk` directory in your repository. ## Summary We hope this comparison of Terraform and Pulumi and our step-by-step guide to bridging a Terraform provider into Pulumi has been useful to you. You should now be able to create a Pulumi provider based on your Terraform provider. Speakeasy can help you generate a Terraform provider based on your OpenAPI specifications. Follow [our documentation](/docs/create-terraform) to enter this exciting ecosystem. Speakeasy is considering adding Pulumi support. [Join our Slack community](https://go.speakeasy.com/slack) to discuss this or for expert advice on Terraform providers. # !mark(15:16) Source: https://speakeasy.com/blog/pydantic-vs-dataclasses import { Callout, Table } from "@/mdx/components"; A massive thank you to [Sydney Runkle](https://x.com/sydneyrunkle) from the [Pydantic](https://pydantic.dev/) team for her invaluable feedback and suggestions on this post!! Python's dynamic typing is one of its greatest strengths. It is the language developers use to get things done without getting bogged down by type definitions and boilerplate code. When prototyping, you don't have time to think about unions, generics, or polymorphism - close your eyes, trust the interpreter to guess your variable's type, and then start working on the next feature. That is, until your prototype takes off and your logs are littered with `TypeError: 'NoneType' object is not iterable` or `TypeError: unsupported operand type(s) for /: 'str' and 'int'`. You might blame the users for adding units in the amount field, or the frontend devs for posting `null` instead of `[]`. So you fix the bug with another `if` statement, a `try` block, or the tenth validation function you've written this week. No time for reflection, just keep shipping, right? The ball of twine must grow. We all know there is a better way. Python has had type annotations for years, and data classes and typed dictionaries allow us to document the shapes of the objects we expect. Pydantic is the most comprehensive solution available to enforce type safety and data validation in Python, which is why we chose it for our SDKs at Speakeasy. In this post we'll run through how we got to this conclusion. We'll detail the history of type safety in Python and explain the differences between: type annotations, data classes, TypedDicts, and finally, Pydantic. ## If It Walks Like a Duck and It Quacks Like a Duck, Then It Must Be a Duck Python is a [duck-typed language](https://docs.python.org/3/glossary.html#term-duck-typing). In a duck-typed language, an object's type is determined by its behavior at runtime, based on the parts of the object that are actually used. Duck-typing makes it easier to write generic code that works with different types of objects. If your code expects a `Duck` object to make it quack, Python doesn't care if the object is a `Mallard` or a `RubberDuck`. From Python's perspective, anything with a `quack` method is a `Duck`: ```python class Duck: def quack(self): print("Quack!") class Mallard: def quack(self): print("Quack!") def make_duck_quack(duck): duck.quack() make_duck_quack(Duck()) # prints "Quack!" make_duck_quack(Mallard()) # prints "Quack!" ``` This code runs without errors, even though `make_duck_quack` expects a `Duck` object in our mental model, and we pass it a `Mallard` object. The `Mallard` object has a `quack` method, so it behaves like a `Duck` object. One of the reasons for Python's popularity is its flexibility. You can write generic and reusable code without worrying about the specific object types. But this flexibility comes at a cost. If you pass the wrong type of object to a function you'll only find out at runtime, leading to bugs that are difficult to track down. This was the motivation behind developing type annotations. ## Type Annotations Type annotations were introduced in Python 3.5 to add optional type hints to your code ([PEP 484](https://www.python.org/dev/peps/pep-0484/)). Type hints can help you catch bugs while you are still writing your code by telling you when you pass the wrong type of object to a function. To make the most of these type hints, many developers use type checkers. Type checkers are tools that analyze your Python code without running it, looking for potential type-related errors. One popular type checker is [Pylance](https://marketplace.visualstudio.com/items?itemName=ms-python.vscode-pylance), a Visual Studio Code Extension that checks your Python code for type mismatches and shows you hints in your IDE. If you're not using VS Code, [Pyright](https://github.com/microsoft/pyright/tree/main) has similar functionality and can be run from the [command line](https://microsoft.github.io/pyright/#/command-line) or as an [extension](https://microsoft.github.io/pyright/#/installation) to many text editors. Here's how you can add type hints to the `make_duck_quack` function: ```python class Duck: def quack(self): print("Quack!") class RubberDuck: def quack(self): print("Quack!") def make_duck_quack(duck: Duck): duck.quack() make_duck_quack(Duck()) # prints "Quack!" make_duck_quack(RubberDuck()) # Pylance will show the hint: Argument 1 to "make_duck_quack" has incompatible type "RubberDuck"; expected "Duck". ``` Now, when you pass a `RubberDuck` object to the `make_duck_quack` function, your IDE hints that there's a type mismatch. Using annotations won't prevent you from running the code if there is a type mismatch, but it can help you catch bugs during development. This covers type annotations for functions, but what about classes? We can use data classes to define a class with specific types for its fields. ## Data Classes Data classes were introduced in Python 3.7 ([PEP 557](https://www.python.org/dev/peps/pep-0557/)) as a convenient way to create classes that are primarily used to store data. Data classes automatically generate special methods like `__init__()`, `__repr__()`, and `__eq__()`, reducing boilerplate code. This feature aligns perfectly with our goal of making type-safe code easier to write. By using data classes, we can define a class with specific types for its fields while writing less code than we would with a traditional class definition. Here's an example: ```python # !mark(17[23:25]) from dataclasses import dataclass @dataclass class Duck: name: str age: int def quack(self): print(f"{self.name} says: Quack!") donald = Duck("Donald", 5) print(donald) # Duck(name='Donald', age=5) donald.quack() # Donald says: Quack! daffy = Duck("Daffy", "3") # Pylance will show the hint: Argument of type "Literal['3']" cannot be assigned to parameter "age" of type "int" in function "__init__". ``` We define a `Duck` data class with two fields: `name` and `age`. When we create a new `Duck` object and pass in values, the data class automatically generates an `__init__()` method that initializes the object with these values. In the data class definition, the type hints specify that the `name` field should be a string and that `age` should be an integer. If we create a `Duck` object with the wrong data types, the IDE hints that there's a type mismatch in the `__init__` method. We get a level of type safety that wasn't there before, but at runtime, the data class still accepts any value for the fields, even if they don't match the type hints. Data classes make it convenient to define classes that store data, but they don't enforce type safety. What if we're building an SDK and want to help users pass the right types of objects to functions? Using `TypedDict` types can help with that. ## TypedDict Types Introduced in Python 3.8 ([PEP 589](https://www.python.org/dev/peps/pep-0589/)), `TypedDict` lets you define specific key and value types for dictionaries, making it particularly useful when working with JSON-like data structures: ```python # !mark(29[20:22]) from typing import TypedDict class DuckStats(TypedDict): name: str age: int feather_count: int def describe_duck(stats: DuckStats) -> str: return f"{stats['name']} is {stats['age']} years old and has {stats['feather_count']} feathers." print( describe_duck( { "name": "Donald", "age": 5, "feather_count": 3000, } ) ) # Output: Donald is 5 years old and has 3000 feathers. print( describe_duck( { "name": "Daffy", "age": "3", # Pylance will show the hint: Argument of type "Literal['3']" cannot be assigned to parameter "age" of type "int" in function "describe_duck" "feather_count": 5000, } ) ) ``` In this example, we define a `DuckStats` `TypedDict` with three keys: `name`, `age`, and `feather_count`. The type hints in the `TypedDict` definition specify that the `name` key should have a string value, while the `age` and `feather_count` keys should have integer values. When we pass a dictionary to the `describe_duck` function, the IDE will show us a hint if there is a type mismatch in the dictionary values. This can help us catch bugs early and ensure that the data we are working with has the correct types. While we now have type hints for dictionaries, data passed to our functions from the outside world are still unvalidated. Users can pass in the wrong types of values and we won't find out until runtime. This brings us to Pydantic. ## Pydantic Pydantic is a data validation library for Python that enforces type hints at runtime. It helps developers with the following: 1. Data Validation: Pydantic ensures that data conforms to the defined types and constraints. 2. Data Parsing: Pydantic can convert input data into the appropriate Python types. 3. Serialization: Pydantic makes it easy to convert Python objects into JSON-compatible formats. 4. Deserialization: It can transform JSON-like data into Python objects. These Pydantic functionalities are particularly useful when working with APIs that send and receive JSON data, or when processing user inputs. Here's how you can use Pydantic to define a data model for a duck: ```python from pydantic import BaseModel, Field, ValidationError class Duck(BaseModel): name: str age: int = Field(gt=0) feather_count: int | None = Field(default=None, ge=0) # Correct initialization try: duck = Duck(name="Donald", age=5, feather_count=3000) print(duck) # Duck(name='Donald', age=5, feather_count=3000) except ValidationError as e: print(f"Validation Error:\n{e}") # Faulty initialization try: invalid_duck = Duck(name="Daffy", age=0, feather_count=-1) print(invalid_duck) except ValidationError as e: print(f"Validation Error:\n{e}") ``` In this example, we define a `Duck` data model with three fields: `name`, `age`, and `feather_count`. The `name` field is required and should have a string value, while the `age` and `feather_count` fields are optional and should have integer values. We use the `Field` class from Pydantic to define additional constraints for the fields. For example, we specify that the `age` field should be greater than or equal to zero, and the `feather_count` field should be greater than or equal to zero, or `None`. In Python 3.10 and later, we can use the `|` operator for union types ([PEP 604](https://www.python.org/dev/peps/pep-0604/)), allowing us to write `int | None` instead of `Union[int, None]`. When we try to create an invalid `Duck` instance, Pydantic raises a `ValidationError`. The error message is detailed and helpful: ```bash Validation Error: 2 validation errors for Duck age Input should be greater than 0 [type=greater_than, input_value=0, input_type=int] # link[35:80] https://errors.pydantic.dev/2.8/v/greater_than For further information visit https://errors.pydantic.dev/2.8/v/greater_than feather_count Input should be greater than or equal to 0 [type=greater_than_equal, input_value=-1, input_type=int] # link[35:86] https://errors.pydantic.dev/2.8/v/greater_than_equal For further information visit https://errors.pydantic.dev/2.8/v/greater_than_equal ``` This error message clearly indicates which fields failed validation and why. It specifies that: 1. The 'age' should be greater than 0, but we provided `0`. 2. The 'feather_count' should be greater than or equal to 0, but we provided `-1`. Detailed error messages make it much easier to identify and fix data validation issues, especially when working with complex data structures or processing user inputs. ## Simplifying Function Validation with Pydantic While we've seen how Pydantic can validate data in models, it can also be used to validate function arguments directly. This can simplify our code while making it safer to run. Let's revisit our `describe_duck` function using Pydantic's `validate_call` decorator: ```python from pydantic import BaseModel, Field, validate_call class DuckDescription(BaseModel): name: str age: int = Field(gt=0) feather_count: int = Field(gt=0) @validate_call def describe_duck(duck: DuckDescription) -> str: return f"{duck.name} is {duck.age} years old and has {duck.feather_count} feathers." # Valid input print(describe_duck(DuckDescription(name="Donald", age=5, feather_count=3000))) # Output: Donald is 5 years old and has 3000 feathers. # Invalid input try: print(describe_duck(DuckDescription(name="Daffy", age=0, feather_count=-1))) except ValueError as e: print(f"Validation Error: {e}") # Validation Error: 2 validation errors for DuckDescription # age # Input should be greater than 0 [type=greater_than, input_value=0, input_type=int] # For further information visit https://errors.pydantic.dev/2.8/v/greater_than # feather_count # Input should be greater than 0 [type=greater_than, input_value=-1, input_type=int] # For further information visit https://errors.pydantic.dev/2.8/v/greater_than ``` In this example, we made the following changes: 1. We defined a `DuckDescription` Pydantic model to represent the expected structure and types of our duck data. 2. We used the `@validate_call` decorator on our `describe_duck` function. This decorator automatically validates the function's arguments based on the type annotations. 3. The function now expects a `DuckDescription` object instead of separate parameters. This ensures that all the data is validated as a unit before the function is called. 4. We simplified the function body since we can now be confident that the data is valid and of the correct type. By using Pydantic's `@validate_call` decorator, we made our function safer and easier to read. ## Comparing Python Typing Methods The table below summarizes the key differences between the Python typing methods we discussed. Keep in mind that some points may have exceptions or nuances depending on your specific use case. The table is meant to provide a general overview only.
## Why Speakeasy Chose Pydantic At Speakeasy, we chose Pydantic as the primary tool for data validation and serialization in the Python SDKs we create. After our initial Python release, support for Pydantic was one of the most requested features from our users. Pydantic provides a great balance between flexibility and type safety. And because Pydantic uses Rust under the hood, it has a negligible performance overhead compared to other third-party data validation libraries. SDKs are an ideal use case for Pydantic, providing automatic data validation and serialization for the data structures that API users interact with. By working with the Pydantic team, we've contributed to the development of features that make Pydantic even better suited for SDK development. ## The Value of Runtime Type Safety To illustrate the value of runtime type safety, consider a scenario where we are building an API that receives JSON data from a client to represent an order from a shop. Let's use a `TypedDict` to define the shape of the order data: ```python from typing import TypedDict class Order(TypedDict): customer_name: str quantity: int unit_price: float def calculate_order_total(order: Order) -> float: return order["quantity"] * order["unit_price"] print( calculate_order_total( { "customer_name": "Alex", "quantity": 10, "unit_price": 5, } ) ) # Output: 50 ``` In this example, we define an `Order` `TypedDict` with three keys: `customer_name`, `quantity`, and `unit_price`. We then create an `order_data` dictionary with values for these keys and pass it to the `calculate_order_total` function. The `calculate_order_total` function multiplies the `quantity` and `unit_price` values from the `order` dictionary to calculate the total order amount. It works fine when the `order_data` dictionary has the correct types of values, but what if the client sends us invalid data? ```python print( calculate_order_total( { "customer_name": "Sam", "quantity": 10, "unit_price": "5", } ) ) # Output: 5555555555 ``` In this case, the client sends us a string value for the `unit_price` key instead of a float. Since Python is a duck-typed language, the code will still run without errors, but the result will be incorrect. This is a common source of bugs in Python code, especially when working with JSON data from external sources. Now, let's see how we can use Pydantic to define a data model for the order data and enforce type safety at runtime: ```python from pydantic import BaseModel, computed_field class Order(BaseModel): customer_name: str quantity: int unit_price: float @computed_field def calculate_total(self) -> float: return self.quantity * self.unit_price order = Order( customer_name="Sam", quantity=10, unit_price="5", ) print(order.calculate_total) # Output: 50.0 ``` In this case, Pydantic converts the string `"5"` to a float value of `5.0` for the `unit_price` field. The automatic type coercion prevents errors and ensures the data is in the correct format. Pydantic enforces type safety at runtime, but don't we lose the simplicity of passing dictionaries around? But we don't have to give up on dictionaries. ## Using Typed Dictionaries With Pydantic Models In some cases, you may want to accept both `TypedDict` and Pydantic models as input to your functions. You can achieve this by using a union type in your function signature: ```python from typing import TypedDict from pydantic import BaseModel class OrderTypedDict(TypedDict): customer_name: str quantity: int unit_price: float class Order(BaseModel): customer_name: str quantity: int unit_price: float def calculate_order_total(order: Order | OrderTypedDict) -> float: if not isinstance(order, BaseModel): order = Order(**order) return order.quantity * order.unit_price print( calculate_order_total( { "customer_name": "Sam", "quantity": 10, "unit_price": "5", } ) ) # Output: 50.0 ``` In this example, we define an `OrderTypedDict` `TypedDict` and an `Order` Pydantic model for the order data. We then define a `calculate_order_total` function to accept a union type of `Order` and `OrderTypedDict`. If the input is a `TypedDict`, it'll be converted to a Pydantic model before performing the calculation. Now our function can accept both `TypedDict` and Pydantic models as input, providing us flexibility while still enforcing type safety at runtime. Speakeasy SDKs employ this pattern so users can pass in either dictionaries or Pydantic models to the SDK functions, reducing the friction of using the SDK while maintaining type safety. ## Conclusion To learn more about how we use Pydantic in our SDKs, see our post about [Python Generation with Async & Pydantic Support](/post/release-python-v2-alpha). # python-alpha-release-pydantic-async Source: https://speakeasy.com/blog/python-alpha-release-pydantic-async import { Callout, ReactPlayer } from "@/lib/mdx/components"; One benefit of having a UK-based engineering hub — no Independence Day! As the US celebrated last week, our UK colleagues were hard at work on the next iteration of our Python SDK Generator. And it's thanks to those efforts that we're able to announce the alpha release of our new Python Generator with support for Async & Pydantic models! ## Python Alpha Release: Pydantic & Async
The new generator takes full advantage of the best tooling available in the Python ecosystem to introduce true end-to-end type safety, support for asynchronous operations, and a streamlined developer experience: - Full type safety with [Pydantic](https://github.com/pydantic/pydantic) models for all request and response objects, - Support for both asynchronous and synchronous method calls using `HTTPX`, - Support for typed dicts as method inputs for an ergonomic interface, - `Poetry` for dependency management and packaging, - Improved IDE compatibility for a better type checking experience, - A DRYer and more maintainable internal library codebase. Check out the [release post](/post/release-python-v2-alpha) for the full details. If you want to test the new Python Generator, please fill out [our Typeform](https://speakeasyapi.typeform.com/python-v2), or message us on Slack. We will provide you with the necessary information to get started. --- ## TypeScript Bundle Size Reduction Bundle Size does matter! Which is why we've been working hard to reduce the size of our TypeScript SDKs, and we're happy to announce that we've made significant progress. Bundle size has dropped an average of 30% with some SDKs seeing reductions of up to 50%. This is down to a couple recent efforts: - **Support for ESM** - We've implemented support for ECMAScript Modules (ESM) alongside CommonJS. This dual bundling approach improves the ability to tree-shake by allowing developers to choose the format that works best for their needs. - **DRYer library code** - We intentionally prioritize readability and debuggability in our SDKs, but that can sometimes mean duplication. We identified some key areas where we could reduce duplication without sacrificing readability. This is just the beginning. With the upcoming release of Zod v4 we'll be able to tree-shake the Zod dependency, and further reduce the bundle size. Stay tuned for further updates! ## 🐝 New Features and Bug Fixes 🐛 Based on the most recent CLI version: [**Speakeasy v2.361.11**](https://github.com/speakeasy-api/openapi-generation/releases/tag/v2.361.11) ### The Platform 🐛 Fix: Correctly display `x-speakeasy-error-message` \ 🐛 Fix: Patch broken usage snippets for SSE-based SDK methods ### TypeScript 🐝 Feat: Support building SDKs to ESM or ESM and CJS \ 🐝 Feat: Refactored Zod Schemas for tree-shaking \ 🐛 Fix: Omit barrel exports in ESM for unused model scopes \ 🐛 Fix: Added missing imports for open enums in TS ### Python 🐛 Fix: use None as arg default instead of UNSET for optional method arguments ### Terraform 🐝 Feat: Support for custom plan modifiers \ 🐝 Feat: Derive json schema types from const values when not specified ### Go 🐝 Feat: Move retry logic out of utils and into a public package ### Java 🐛 Fix: Patched compilation error in client credentials hook when security not flattened # Create an opener with authentication handler Source: https://speakeasy.com/blog/python-http-clients-requests-vs-httpx-vs-aiohttp import { Table } from "@/mdx/components"; Anyone who's been using Python for more than a minute has come across the `Requests` library. It is so ubiquitous, some may have thought it was part of the standard library. Requests is so intuitive that writing `r = requests.get` has become muscle memory. In contrast, any script using Python's built-in [`urllib`](https://docs.python.org/3/library/urllib.html) starts with a trip to the Python docs. But Python has evolved, and simply defaulting to Requests is no longer an option. While `Requests` remains a solid choice for short synchronous scripts, newer libraries like `HTTPX` and `AIOHTTP` are better suited for modern Python, especially when it comes to asynchronous programming. Let's compare these three popular HTTP clients for Python: [`Requests`](https://github.com/psf/requests), [`HTTPX`](https://github.com/projectdiscovery/httpx), and [`AIOHTTP`](https://docs.aiohttp.org/en/stable/). We'll explore their strengths, weaknesses, and ideal use cases to help you choose the right tool for your next project. ## In The Beginning, Guido Created Urllib Before we dive into our comparison of modern HTTP libraries, it's worth taking a brief look at where it all began: Python's built-in `urllib` module. `urllib` has been part of Python's standard library since the early days. It was designed to be a comprehensive toolkit for URL handling and network operations. However, its API is notoriously complex and unintuitive, often requiring multiple steps to perform even simple HTTP requests. Here's a basic example of making a GET request with `urllib`: ```python filename="urllib_basic.py" from urllib.request import urlopen with urlopen('https://api.github.com') as response: body = response.read() print(body) ``` While this might seem straightforward for a simple GET request, things quickly become more complicated when dealing with headers, POST requests, or authentication. For instance, here's how you might make a request with authentication: ```python filename="urllib_example.py" import urllib.request import json url = 'http://httpbin.org/basic-auth/user/passwd' username = 'user' password = 'passwd' password_mgr = urllib.request.HTTPPasswordMgrWithDefaultRealm() password_mgr.add_password(None, url, username, password) auth_handler = urllib.request.HTTPBasicAuthHandler(password_mgr) opener = urllib.request.build_opener(auth_handler) # Make the request with opener.open(url) as response: raw_data = response.read() encoding = response.info().get_content_charset('utf-8') data = json.loads(raw_data.decode(encoding)) print(data) ``` In this example, we create an authentication handler and an opener to make the request. We then read the response, decode it, and parse the JSON data. The verbosity and complexity of `urllib` led to the creation of third-party libraries that aimed to simplify HTTP requests in Python. ## Requests: HTTP For Humans™️ In 2011 (on Valentine's day, no less), Kenneth Reitz released the [Requests](https://github.com/psf/requests) library, designed to make HTTP requests as human-friendly as possible. After only two years, [by July 2013, Requests had been downloaded more than 3,300,000 times](https://web.archive.org/web/20130905090055/http://kennethreitz.org/growing-open-source-seeds), and as of [August 2024](https://web.archive.org/web/20240818081841/https://pypistats.org/top), it gets downloaded around 12 million times a day. It turns out [devex is important after all](/docs/introduction/api-devex)! To install Requests, use pip: ```bash pip install requests ``` Let's compare the previous `urllib` examples with their Requests equivalents: ```python filename="requests_example.py" import requests # GET request response = requests.get('https://api.github.com') print(response.text) # request with auth url = 'http://httpbin.org/basic-auth/user/passwd' username = 'user' password = 'passwd' response = requests.get(url, auth=(username, password)) data = response.json() print(data) ``` The simplicity and readability of `Requests` code compared to `urllib` is immediately apparent. `Requests` abstracts away much of the complexity, handling things like authentication headers and JSON responses with ease. Some key features that made `Requests` the de facto standard include: 1. **Automatic content decoding**: `Requests` automatically decodes the response content based on the Content-Type header. 2. **Session persistence**: The `Session` object allows you to persist certain parameters across requests. 3. **Elegant error handling**: `Requests` raises intuitive exceptions for network problems and HTTP errors. 4. **Automatic decompression**: `Requests` automatically decompresses gzip-encoded responses. However, as Python evolved and the use cases for Python expanded, new needs arose that `Requests` wasn't designed to address. In particular, Asynchronous rose as a need which led to the introduction of `asyncio` in Python 3.4. ## `AIOHTTP`: Built for Asyncio [`AIOHTTP`](https://github.com/aio-libs/aiohttp), first released in October 2014, was one of the first libraries to fully embrace Python's asyncio framework. Designed from the ground up for asynchronous operations, it's an excellent choice for high-performance, concurrent applications. Today, `AIOHTTP` is widely used, with around [six million downloads per day](https://web.archive.org/web/20250510011435/https://pypistats.org/packages/aiohttp) as of May 2024. `AIOHTTP` has several key features that set it apart from Requests: 1. **Purely asynchronous**: All operations in `AIOHTTP` are async, allowing for efficient handling of many concurrent connections. 2. **Both client and server**: `AIOHTTP` can be used to create both HTTP clients and servers. 3. **WebSocket support**: It offers full support for WebSocket connections. Install `AIOHTTP` using pip: ```bash pip install aiohttp ``` Here's a basic example of using `AIOHTTP`: ```python filename="aiohttp_basic.py" import aiohttp import asyncio async def fetch(session, url): async with session.get(url) as response: return await response.text() async def main(): async with aiohttp.ClientSession() as session: html = await fetch(session, 'https://api.github.com') print(html) asyncio.run(main()) ``` To really test `AIOHTTP`'s capabilities, you need to run multiple requests concurrently. Here's an example that fetches multiple URLs concurrently: ```python filename="aiohttp_multiple.py" import asyncio import aiohttp import time urls = [ "https://httpbin.org/delay/1", "https://httpbin.org/delay/2", "https://httpbin.org/delay/3", "https://httpbin.org/delay/1", "https://httpbin.org/delay/2", ] async def fetch(session, url, i): try: start_time = time.perf_counter() async with session.get(url) as response: await response.text() elapsed = time.perf_counter() - start_time print(f"Request {i} completed in {elapsed:.2f}s") except asyncio.TimeoutError: print(f"Request {i} timed out") async def async_requests(): start_time = time.perf_counter() async with aiohttp.ClientSession(timeout=aiohttp.ClientTimeout(total=10)) as session: tasks = [fetch(session, url, i) for i, url in enumerate(urls, 1)] await asyncio.gather(*tasks) total_time = time.perf_counter() - start_time print(f"\nTotal time: {total_time:.2f}s") if __name__ == "__main__": asyncio.run(async_requests()) ``` In this example, we're fetching five URLs concurrently, each with a different server-side delay. The script will output the time taken for each request to complete, as well as the total time taken: ```bash filename="output" Request 1 completed in 2.22s Request 4 completed in 2.22s Request 5 completed in 3.20s Request 2 completed in 3.20s Request 3 completed in 4.30s Total time: 4.31s ``` For comparison, here's how you might achieve the same thing using Requests: ```python filename="requests_multiple.py" import requests import time urls = [ "https://httpbin.org/delay/1", "https://httpbin.org/delay/2", "https://httpbin.org/delay/3", "https://httpbin.org/delay/1", "https://httpbin.org/delay/2", ] def sync_requests(): start_time = time.time() with requests.Session() as session: session.timeout = 10.0 for i, url in enumerate(urls, 1): try: response = session.get(url) print(f"Request {i} completed in {response.elapsed.total_seconds():.2f}s") except requests.Timeout: print(f"Request {i} timed out") total_time = time.time() - start_time print(f"\nTotal time: {total_time:.2f}s") if __name__ == "__main__": sync_requests() ``` The output will be similar to the `AIOHTTP` example, but the total time taken will be significantly longer due to the synchronous nature of Requests. ```bash filename="output" Request 1 completed in 3.04s Request 2 completed in 2.57s Request 3 completed in 3.26s Request 4 completed in 1.23s Request 5 completed in 2.49s Total time: 12.61s ``` As you can see, `AIOHTTP`'s asynchronous nature allows it to complete all requests in roughly the time it takes to complete the slowest request, while Requests waits for each request to complete sequentially. While `AIOHTTP` is a powerful library for asynchronous operations, it doesn't provide a synchronous API like Requests. This is where HTTPX comes in. ## `HTTPX`: The Best of Both Worlds [HTTPX](https://github.com/encode/httpx), released by Tom Christie (the author of Django REST framework) in August 2019, aims to combine the best features of Requests and `AIOHTTP`. It provides a synchronous API similar to Requests but also supports asynchronous operations. Key features of `HTTPX` include: 1. **Familiar Requests-like API**: `HTTPX` maintains a similar API to Requests, making it easy for developers to transition. 2. **Both sync and async support**: Unlike Requests or `AIOHTTP`, `HTTPX` supports both synchronous and asynchronous operations. 3. **HTTP/2 support**: `HTTPX` natively supports HTTP/2, allowing for more efficient communication with modern web servers. 4. **Type annotations**: `HTTPX` is fully type-annotated, which improves IDE support and helps catch errors early. To install `HTTPX`, use pip: ```bash pip install httpx ``` Here's a basic example of using `HTTPX` synchronously: ```python filename="httpx_sync.py" import httpx response = httpx.get('https://api.github.com') print(response.status_code) print(response.json()) ``` The code is almost identical to the Requests example, making it easy to switch between the two libraries. However, HTTPX also supports asynchronous operations: ```python filename="httpx_async.py" import asyncio import httpx import time urls = [ "https://httpbin.org/delay/1", "https://httpbin.org/delay/2", "https://httpbin.org/delay/3", "https://httpbin.org/delay/1", "https://httpbin.org/delay/2", ] async def fetch(client, url, i): response = await client.get(url) print(f"Request {i} completed in {response.elapsed.total_seconds():.2f}s") async def async_requests(): start_time = time.time() async with httpx.AsyncClient(timeout=10.0) as client: tasks = [fetch(client, url, i) for i, url in enumerate(urls, 1)] await asyncio.gather(*tasks) total_time = time.time() - start_time print(f"\nTotal time: {total_time:.2f}s") if __name__ == "__main__": asyncio.run(async_requests()) ``` The output will be similar to the AIOHTTP example: ```bash filename="output" Request 1 completed in 1.96s Request 4 completed in 1.96s Request 5 completed in 3.00s Request 2 completed in 3.30s Request 3 completed in 4.42s Total time: 4.44s ``` HTTPX's ability to switch seamlessly between synchronous and asynchronous operations makes it a versatile choice for a wide range of applications. It's especially useful when you need to interact with both synchronous and asynchronous code within the same project. This brings us to the question: which library should you choose for your next Python project? That depends on your specific requirements. ## Choosing the Right HTTP Client for Your Project Here's a quick comparison of the key features of Requests, AIOHTTP, and HTTPX:
## Recommendations 1. If you're working on a simple script or a project that doesn't require asynchronous operations, stick with **`Requests`**. Its simplicity and wide adoption make it an excellent choice for straightforward HTTP tasks. 2. For high-performance asyncio applications, especially those dealing with many concurrent connections or requiring WebSocket support, **`AIOHTTP`** is your best bet. It's particularly well-suited for building scalable web services. 3. If you need the flexibility to use both synchronous and asynchronous code, or if you're looking to future-proof your application with `HTTP/2` support, go with **`HTTPX`**. It's also a great choice if you're familiar with `Requests` but want to start incorporating async operations into your project. ## How Speakeasy Uses `HTTPX` When creating Python SDKs, Speakeasy includes HTTPX as the default HTTP client. This choice allows developers to use our SDKs for synchronous and asynchronous operations. For example, here's how you might use the [Mistral Python SDK](https://github.com/mistralai/client-python) created by Speakeasy to make requests. First, install the SDK: ```bash pip install mistralai ``` Set your [Mistral API key](https://console.mistral.ai/api-keys/) as an environment variable: ```bash export MISTRAL_API_KEY="your-api-key" ``` Here's how you might use the SDK to make a synchronous request: ```python filename="mistral_sync.py" from mistralai import Mistral import os s = Mistral( api_key=os.getenv("MISTRAL_API_KEY", ""), ) res = s.chat.complete(model="mistral-small-latest", messages=[ { "content": "Who is the best French painter? Answer in one short sentence.", "role": "user", }, ]) if res is not None and res.choices: print(res.choices[0].message.content) ``` And here's the same SDK and request using the asynchronous API: ```python filename="mistral_async.py" import asyncio from mistralai import Mistral import os async def main(): s = Mistral( api_key=os.getenv("MISTRAL_API_KEY", ""), ) res = await s.chat.complete_async(model="mistral-small-latest", messages=[ { "content": "Who is the best French painter? Answer in one short sentence.", "role": "user", }, ]) if res is not None: print(res.choices[0].message.content) asyncio.run(main()) ``` Note how the asynchronous version uses the `_async` suffix for the method name, but otherwise the code is almost identical. This consistency makes it easy to switch between synchronous and asynchronous operations as needed. You'll also notice that there is no need to instantiate a different client object for the asynchronous version. SDKs created by Speakeasy allow developers to use the same client object for both synchronous and asynchronous operations. By abstracting away the differences between the modes of operation in HTTPX, Speakeasy reduces boilerplate code and makes your SDKs more user-friendly. To illustrate the value of mixing synchronous and asynchronous operations, consider a scenario where you need to make a synchronous request to fetch some data, then use that data to make multiple asynchronous requests. HTTPX's unified API makes this kind of mixed-mode operation straightforward. ```python filename="mistral_mixed.py" import asyncio from mistralai import Mistral import os # Initialize Mistral client s = Mistral( api_key=os.getenv("MISTRAL_API_KEY", ""), ) def sync_request(): res = s.chat.complete(model="mistral-small-latest", messages=[ { "content": "Who is the best French painter? Answer with only the name of the painter.", "role": "user", }, ]) if res is not None and res.choices: print("Sync request result:", res.choices[0].message.content) return res.choices[0].message.content async def async_request(question): res = await s.chat.complete_async(model="mistral-small-latest", messages=[ { "content": question, "role": "user", }, ]) if res is not None and res.choices: return res.choices[0].message.content return None async def main(): # Make a sync request painter = sync_request() # Make two async requests tasks = [ async_request(f"Name the most iconic painting by {painter}. Answer in one short sentence."), async_request(f"Name one of {painter}'s influences. Answer in one short sentence."), ] results = await asyncio.gather(*tasks) # Print the results of async requests print("Async request 1 result:", results[0]) print("Async request 2 result:", results[1]) if __name__ == "__main__": asyncio.run(main()) ``` In this example, we first make a synchronous request to get the name of a painter. We then use that information to make two asynchronous requests to get more details about the painter. The SDK is only instantiated once, and the same client object is used for both synchronous and asynchronous operations. ## Using a Different HTTP Client in Speakeasy SDKs While HTTPX is the default HTTP client in SDKs created by Speakeasy, you can [easily switch to Requests for synchronous operations](/docs/customize-sdks/custom-http-client) if needed. For example, to use Requests in the Mistral SDK, you can set the `client` parameter when initializing the client: ```python filename="mistral_requests.py" import os import requests from mistralai import Mistral, HttpClient # Define a custom HTTP client using Requests class RequestsHttpClient(HttpClient): def __init__(self): self.session = requests.Session() def send(self, request, **kwargs): return self.session.send(request.prepare()) def build_request( self, method, url, *, content = None, headers = None, **kwargs, ): return requests.Request( method=method, url=url, data=content, headers=headers, ) # Initialize the custom client client = RequestsHttpClient() # Initialize Mistral with the custom client s = Mistral( api_key=os.getenv("MISTRAL_API_KEY", ""), client=client, ) # Use the Mistral client res = s.chat.complete(model="mistral-small-latest", messages=[ { "content": "Who is the best French painter? Answer in one short sentence.", "role": "user", }, ]) if res is not None and res.choices: print(res.choices[0].message.content) ``` In this example, we define a custom `RequestsHttpClient` class that extends `HttpClient` from the Mistral SDK. This class uses the Requests library to send HTTP requests. We then initialize the Mistral client with this custom client, allowing us to use Requests for synchronous operations. # Conclusion To learn more about how we use HTTPX in our SDKs, see our post about [Python Generation with Async & Pydantic Support](/blog/release-python). You can also read more about our Python SDK design principles in our [Python SDK Design Overview](/docs/sdk-design/python/methodology-python). # Check your current version Source: https://speakeasy.com/blog/python-sdk-lazy-loading-performance We shipped a **major performance improvement** to our Python SDKs. Now, every Speakeasy-generated Python SDK will defer loading of modules until they're actually needed — instead of loading everything upfront when the SDK is imported. We've implemented this by making use of Python's forward references and lazy loading of modules. This approach maintains full type safety and IDE autocomplete functionality while completely avoiding the performance penalty of eager imports. ## Why it matters With this update, Python SDKs will show dramatic performance improvements across two key metrics: - **8.5x faster initialization**: In a sample of large customer SDKs, import time was reduced from 1.7 seconds to just 0.2 seconds. Faster imports mean better developer experience: less waiting during development, faster CI/CD test runs, and quicker serverless cold starts - **80% reduced memory footprint**: Similarly, memory usage was cut from ~98MB to ~22MB on initial import ## Performance Impact ### Initialization Time Improvements In the example below, the time taken for the import statement dropped from 1.26 sec to 0.17 sec — **7.5x faster!** This improves the developer experience considerably. **Before lazy loading:** ![Before lazy loading - 1.26 seconds import time](/assets/blog/python-lazy-loading/before-timing.png) **After lazy loading:** ![After lazy loading - 0.17 seconds import time](/assets/blog/python-lazy-loading/after-timing.png) ### Memory Usage Reduction The memory used by the process was reduced from 124 MB to 40MB — a **68% reduction** in memory footprint. **Before lazy loading:** ![Before lazy loading - 124 MB memory usage](/assets/blog/python-lazy-loading/before-memory.png) **After lazy loading:** ![After lazy loading - 40 MB memory usage](/assets/blog/python-lazy-loading/after-memory.png) _Performance measurements were taken using [this script](https://gist.github.com/kanwardeep007/60240afe152ab9645a74279c302b9015)._ ## Key Benefits 1. **Faster Development Cycles**: Reduced import times mean less waiting during development iterations 2. **Improved CI/CD Performance**: Faster test suite execution with quicker SDK imports 3. **Better Serverless Performance**: Reduced cold start times for serverless functions 4. **Lower Resource Costs**: Decreased memory usage translates to lower infrastructure costs 5. **Maintained Developer Experience**: Full type safety and IDE autocomplete functionality preserved ## Technical Implementation Our lazy loading implementation leverages: - **Python Forward References**: Maintaining type hints without eager imports - **Module-level Lazy Loading**: Deferring module initialization until first use - **Preserved Type Safety**: Full static analysis support maintained - **IDE Compatibility**: Autocomplete and IntelliSense continue to work seamlessly ## Getting Started Simply ensure your Speakeasy CLI is upgraded to version v1.549.0: ```bash speakeasy -v # Update to the latest version speakeasy update ``` Every Python SDK generated with this version and above will incorporate these improvements automatically, and your applications will start faster and use less memory! ## What's Next This performance improvement is just the beginning. We're continuously working on optimizing our SDK generation to provide the best possible developer experience. Stay tuned for more performance enhancements across all our supported languages. Try out the new lazy loading feature in your Python SDKs and experience the performance boost firsthand! # Send to Claude Source: https://speakeasy.com/blog/rag-vs-mcp Ask Claude about Django 5.2's new features, and you get this: ![Claude has no knowledge of Django 5.2](/assets/blog/rag-vs-mcp/claude-no-django-knowledge.png) Django 5.2 was released in April 2025. Claude's training data ends in January 2025, so it doesn't know about this release yet. This is the core limitation of LLMs: their knowledge is frozen at training time. They can't access information published after their cutoff date or domain-specific data locked inside your organization. Claude doesn't know your API documentation, your company's internal policies, or yesterday's product updates. To bridge this gap, you have two architectural options: Retrieval-Augmented Generation (RAG) or Model Context Protocol (MCP) servers. Both connect LLMs to external information, but they work differently and excel in different scenarios. This guide compares RAG and MCP through a concrete example: teaching Claude about Django 5.2's new features. You'll learn how each approach works, what it costs in tokens, and when to use which. ## RAG vs MCP: Different problems, different solutions **RAG** is an architecture pattern for semantic search. Documents are embedded into vectors, stored in a database, and retrieved based on semantic similarity to user queries. **MCP** is a protocol for connecting LLMs to external systems. It standardizes how LLMs call tools and access data sources, and whether those tools perform RAG searches, query databases, or hit APIs. They're not competing approaches. RAG solves issues like, "How do I search documents semantically?" while MCP solves problems like, "How do I connect my LLM to external systems?" ## What is RAG? RAG combines information retrieval with text generation. Instead of relying only on training data, the LLM searches external databases for relevant context before generating an answer. RAG works in three steps: 1. **Retrieval:** The system sends the user's question to a retriever, which is usually a vector database like [ChromaDB](https://docs.trychroma.com/docs/overview/introduction), [Pinecone](https://docs.pinecone.io/guides/get-started/quickstart), or [Weaviate](https://weaviate.io/). The retriever then searches for semantically similar content and returns the most relevant chunks. 2. **Context injection:** The system adds retrieved information to the LLM prompt. ```python prompt = f"""You are a helpful nutritional assistant. Here is relevant information from our nutrition database: {context} Based on this information, please answer the following question: {question} Provide a clear answer using only the context above. If the context doesn't contain enough information, say so.""" ``` 3. **Generation:** The LLM generates an answer using both the retrieved context and the question, grounding its response in actual documentation rather than relying on training data alone. ![RAG example](/assets/blog/rag-vs-mcp/rag-architecture-diagram.png) In the diagram above, a user asks, "What's new in django.contrib.gis?" The query goes to a vector database, which returns relevant snippets like, "Django 5.2 adds MultiCurve geometry." The LLM receives these snippets with instructions to answer only using the provided documents, resulting in a response like, "Django 5.2 adds MultiCurve support..." ## What is MCP? The [Model Context Protocol](https://modelcontextprotocol.io/docs/getting-started/intro) standardizes how LLMs access external tools and data sources. Without MCP, each LLM provider uses different tool integration methods. Anthropic has tool use, OpenAI has function calling, and each uses its own schema. Building an integration for Claude means rebuilding it from scratch for GPT-5. MCP provides one consistent interface. An MCP server works with any MCP-compatible system such as Claude, GPT-5, or custom applications. The protocol defines how tools, databases, and APIs get exposed, eliminating the need for provider-specific implementations. ## Why is RAG preferred over MCP resources? To pass documents as context in MCP servers, MCP provides resource primitives that LLMs can consult to enrich their contexts before returning a response. But here is the thing: there is no processing or anything of the document. So, the context is just dumped into the context window, which can easily become bloated. RAG is then preferred over MCP resources due to the vectorization that makes the document much more digestible, reducing token consumption and saving money in the long run. ## RAG vs MCP in practice Both RAG and MCP can provide Claude Anthropic with Django 5.2 documentation, but in different ways. Let's see how RAG and MCP handle the same task of finding information about new features in Django 5.2. ### RAG: Semantic search Building a RAG system for Django documentation requires three components: - A document processor that extracts and chunks text - An embedder that converts text to vectors - A vector database for storage This implementation uses the PyPDF2 library for PDF extraction, the Sentence Transformers library for embeddings, ChromaDB for vector storage, and Anthropic for answer generation. #### Indexing the documentation The first step is to extract and preprocess the documentation. The following code reads the Django PDF, splits it into overlapping text segments to preserve context, embeds those chunks in vectors, and stores everything in ChromaDB for semantic search: ```python from pathlib import Path from PyPDF2 import PdfReader from sentence_transformers import SentenceTransformer import chromadb pdf_path = Path("django.pdf") reader = PdfReader(str(pdf_path)) text = "\n\n".join([p.extract_text() for p in reader.pages]) words = text.split() chunks = [" ".join(words[i:i+500]) for i in range(0, len(words), 450)] embeddings = SentenceTransformer("all-MiniLM-L6-v2").encode(chunks) client = chromadb.PersistentClient(path="./chroma_db") collection = client.create_collection("django_docs") collection.add(ids=[f"chunk_{i}" for i in range(len(chunks))], embeddings=embeddings.tolist(), documents=chunks) ``` Here, we create 500-word chunks with 50-word overlaps. This overlap is essential because it ensures that concepts spanning chunk boundaries remain intact and searchable. We then convert each chunk into a numerical embedding, which is a 384-dimensional vector that captures its semantic meaning. The `all-MiniLM-L6-v2` model handles this conversion. It's lightweight and fast, but still maintains good semantic accuracy. #### Querying the system When a user asks a question about Django 5.2, the system embeds the question using the same `all-MiniLM-L6-v2` model, retrieves the most relevant chunks from ChromaDB based on semantic similarity, and sends those chunks to Claude. Claude receives the retrieved documentation with instructions to answer using only the provided context. ```python import os from sentence_transformers import SentenceTransformer import chromadb import anthropic from dotenv import load_dotenv load_dotenv() model = SentenceTransformer("all-MiniLM-L6-v2") client = chromadb.PersistentClient(path="./chroma_db") collection = client.get_collection("django_docs") query = "Django 5.2 new features" q_embed = model.encode(query).tolist() results = collection.query(query_embeddings=[q_embed], n_results=3) context = "\n\n".join(results["documents"][0]) client = anthropic.Anthropic(api_key=os.getenv("ANTHROPIC_API_KEY")) response = client.messages.create( model="claude-sonnet-4-20250514", max_tokens=1024, messages=[{"role": "user", "content": f"{context}\n\nQuestion: {query}"}] ) print("Answer:") print(response.content[0].text) print(f"\nTokens used: {response.usage.input_tokens + response.usage.output_tokens}") ``` The system retrieves the three chunks most similar to the query, and formats them into the prompt along with the question. Claude then generates an answer grounded in the actual Django documentation rather than its training data. ### RAG outcome When a user asks about Django 5.2, the system embeds the query in a vector, matches it against the ChromaDB content, and retrieves the three 500-word chunks most semantically similar to the query: ```python import os import anthropic from mcp.client.stdio import stdio_client from mcp import ClientSession, StdioServerParameters import asyncio import json from dotenv import load_dotenv load_dotenv() async def query_mcp(question: str): # 1. Connect to MCP server server_params = StdioServerParameters( command="python3", args=["mcp_server.py"], env=None ) async with stdio_client(server_params) as (read, write): async with ClientSession(read, write) as session: await session.initialize() # 2. Call search tool result = await session.call_tool( "search_django_docs", arguments={"query": question} ) return json.loads(result.content[0].text) # Run query question = "Django 5.2 new features" mcp_results = asyncio.run(query_mcp(question)) # 3. Format results and send to Claude mcp_docs = "\n\n".join([ f"[Page {r['page']}]\n{r['text']}" for r in mcp_results["results"] ]) anthropic_client = anthropic.Anthropic(api_key=os.getenv("ANTHROPIC_API_KEY")) response = anthropic_client.messages.create( model="claude-sonnet-4-20250514", max_tokens=200, messages=[{ "role": "user", "content": f"Based on this Django documentation:\n\n{mcp_docs}\n\nQuestion: {question}\n\nProvide a short, direct answer." }] ) print("Answer:") print(response.content[0].text) print(f"\nTokens used: {response.usage.input_tokens + response.usage.output_tokens}") ``` The three chunks cover the Django 5.2 release notes, new features, and improvements. They're semantically related to "new features", even if they don't use the exact words used in the prompt. RAG understands that questions about releases relate to feature updates and documentation changes. ![Using RAG](/assets/blog/rag-vs-mcp/rag-implementation-result.png) The RAG semantic search uses: - **Tokens:** 9,619 - **Cost:** $0.036 - **Response time:** 8.89s ### MCP: Keyword search Building an MCP server for Django documentation requires a different architectural approach. Instead of pre-indexing prompts with embeddings, we expose the Django PDF through **Tools** that Claude can call on demand. MCP servers communicate via JSON-RPC over stdio, making them easy to integrate with Claude Desktop or Claude Code. #### MCP data An MCP server mainly provides two types of primitives: - **Resources** are read-only data endpoints (like `django://docs/models`). - **Tools** are callable functions that perform searches, lookups, or actions. #### MCP implementation The server loads the Django PDF once at startup, then provides a `search_django_docs` tool that performs keyword matching across all pages. When Claude needs information about Django 5.2, it calls this tool with a search query and receives all matching pages as JSON. ```python #!/usr/bin/env python3 """MCP Server for Django Documentation""" from pathlib import Path from mcp.server import Server from mcp.server.stdio import stdio_server from mcp.types import Tool, TextContent from PyPDF2 import PdfReader import json class DjangoMCPServer: def __init__(self): self.server = Server("django-docs") self.documentation_pages = [] self._load_documentation() self._setup_handlers() def _load_documentation(self): """Load Django PDF once at startup""" pdf_path = Path("django.pdf") if not pdf_path.exists(): return reader = PdfReader(str(pdf_path)) self.documentation_pages = [ page.extract_text() for page in reader.pages ] def _search_documentation(self, query: str, max_pages: int = 50): """Keyword search across all pages""" query_words = query.lower().split() matching_pages = [] for i, page_text in enumerate(self.documentation_pages): page_lower = page_text.lower() # Check if any query word appears in this page if any(word in page_lower for word in query_words): matching_pages.append({ "text": page_text, "page_num": i + 1 }) if len(matching_pages) >= max_pages: break return matching_pages def _setup_handlers(self): """Register MCP tool handlers""" @self.server.list_tools() async def list_tools(): return [ Tool( name="search_django_docs", description="Search Django documentation and return all matching pages", inputSchema={ "type": "object", "properties": { "query": { "type": "string", "description": "Search query (e.g., 'models', 'authentication')" } }, "required": ["query"] } ) ] @self.server.call_tool() async def call_tool(name: str, arguments: dict): if name == "search_django_docs": query = arguments.get("query", "") results = self._search_documentation(query) formatted_results = { "query": query, "results_count": len(results), "results": [ { "page": r["page_num"], "text": r["text"] } for r in results ] } return [TextContent( type="text", text=json.dumps(formatted_results) )] return [TextContent( type="text", text=json.dumps({"error": f"Unknown tool: {name}"}) )] async def run(self): """Start the MCP server""" async with stdio_server() as (read_stream, write_stream): await self.server.run( read_stream, write_stream, self.server.create_initialization_options() ) def main(): import asyncio server = DjangoMCPServer() asyncio.run(server.run()) if __name__ == "__main__": main() ``` The server performs simple keyword matching. When Claude asks about new Django 5.2 features, the server finds all documentation pages containing words like "Django", "5.2", "new", or "features". ### MCP outcome The MCP server splits the query into keywords, searches for any pages containing those words, and returns up to 50 matching pages: ```python from mcp import ClientSession, StdioServerParameters, stdio_client import asyncio import json async def query_mcp(question: str): # 1. Connect to MCP server server_params = StdioServerParameters( command="python3", args=["mcp_server.py"], env=None ) async with stdio_client(server_params) as (read, write): async with ClientSession(read, write) as session: await session.initialize() # 2. Call search tool result = await session.call_tool( "search_django_docs", arguments={"query": question} ) return json.loads(result.content[0].text) # Run query question = "Django 5.2 new features" mcp_results = asyncio.run(query_mcp(question)) # 3. Format results and send to Claude mcp_docs = "\n\n".join([ f"[Page {r['page']}]\n{r['text']}" for r in mcp_results["results"] ]) response = anthropic_client.messages.create( model="claude-sonnet-4-20250514", max_tokens=200, messages=[{ "role": "user", "content": f"I searched Django docs for: \"{question}\"\n\nFound {mcp_results['results_count']} pages:\n\n{mcp_docs}\n\nProvide a short, direct answer." }] ) ``` The server returns 50 pages containing words like "Django", "5.2", "new", and "features". This includes relevant content, such as release notes and feature documentation, alongside many irrelevant pages: configuration files mentioning "new" settings, migration guides with "Django" in headers, tutorial pages with "features" in titles, and 45 other loosely related pages. Claude reads through everything to find the answer. ![Using MCP servers](/assets/blog/rag-vs-mcp/mcp-implementation-result.png) The MCP implementation provided an incomplete answer. The naive keyword search found pages containing "Django", "features", "new", or "5.2" throughout the 2,000+ page document. However, the implementation hit a hard limit: Claude's API has a maximum context window. The system can only send the first 50 matching pages to stay within token limits. The most relevant release notes pages weren't necessarily among the first 50 pages. The keyword matching has no way to prioritize which pages are most relevant because it just returns them in document order. Even if we had unlimited context, sending hundreds of pages per query would be impractical: costs would exceed $9 per query and response times would stretch beyond 90 seconds. The MCP keyword search uses: - **Tokens:** 30,048 - **Cost:** $0.0925 - **Response time:** 30.41 seconds ### Comparing outcomes Looking at the resource use for each implementation, it's clear that RAG is the more efficient option. Generating a response with RAG uses less than a third of the tokens and time it takes with MCP. | Metric | RAG | MCP | | ------------------ | ------ | ------- | | **Input tokens** | 9,619 | 30,048 | | **Cost per query** | $0.036 | $0.0925 | | **Response time** | 8.89s | 30.41s | This stark difference is due to the use of vectorization in the RAG implementation. By processing documents into chunks and sending only three chunks to Claude, RAG keeps both the token use and the monetary cost of each query low. In contrast, the MCP server loads the Django document as an MCP resource primitive that is not processed. By passing 50 unprocessed document pages in to the context window, MCP keyword matching uses more than triple the resources of a RAG semantic search. The RAG implementation also resulted in a more complete answer. Because RAG uses semantic similarity, it provides more focused, relevant context. The embedding model converted the "Django 5.2 new features" query into a vector that's mathematically similar to vectors for "release notes", "version updates", and "feature announcements", enabling the system to retrieve content based on conceptual proximity. Without the same semantic reasoning, the MCP keyword search delivered the first 50 pages containing "Django", "new", or "features" rather than the 50 most relevant pages containing those keywords. ## When to use MCP MCP shines when working with dynamic, structured data that changes frequently. For example, consider running an e-commerce platform with 100,000+ products. Your prices change daily, the inventory updates in real time, and you constantly add new products. With RAG, every time something changes, you'd need to re-embed that data and update your vector database. Given your extensive product catalog, you'd need to regenerate embeddings for thousands of items daily. The embedding process alone could cost $5-10 per day and take 10-30 minutes to complete. You'd always have a sync lag between your real data and the data the LLM can access. With MCP, you could use tools like `get_product_info` and `search_products` to access the actual platform data: ```python @mcp.tool() def get_product_info(product_id: str) -> dict: response = requests.get(f"https://api.yourstore.com/products/{product_id}") return response.json() @mcp.tool() def search_products(query: str, category: str = None) -> list: return api.search(query=query, in_stock=True, category=category) ``` This pattern works beautifully for any structured data that changes often, including user accounts, order statuses, inventory levels, API rate limits, and server metrics. MCP also enables actions, enabling agents not just to read data but also to create orders, update records, or trigger workflows. RAG remains the better choice for static content, like documentation, where semantic search matters and the content rarely changes. The ideal architecture often combines both: using RAG to search product documentation and tutorials, and MCP to handle live inventory lookups and order placement. ## Combining RAG and MCP Given their differences, you may think you shouldn't use RAG and MCP together, but they combine perfectly. In fact, MCP tools can perform RAG queries, acting as the bridge between Claude and your vector database. For example, instead of Claude directly accessing your vector store, you can build an MCP tool that handles the entire RAG pipeline processes — embedding the query, searching vectors, and returning relevant chunks. From Claude's perspective, it just calls a tool, but under the hood, that tool conducts a sophisticated semantic search. ```python @mcp.tool() async def search_documentation(query: str, top_k: int = 3) -> dict: """ Search technical documentation using semantic similarity. Returns the most relevant documentation chunks. """ # 1. Embed the query query_embedding = embedder.encode(query) # 2. Search vector database results = collection.query( query_embeddings=[query_embedding.tolist()], n_results=top_k ) # 3. Return formatted results return { "query": query, "chunks": [ { "text": doc, "page": meta["page_num"], "similarity": 1 - distance } for doc, meta, distance in zip( results["documents"][0], results["metadatas"][0], results["distances"][0] ) ] } ``` This architecture gives you the best of both worlds. You benefit from RAG's semantic search and efficient token usage, while MCP provides standardization, discoverability, and easy integration with Claude Desktop and other MCP clients. The vector database stays behind your MCP server, handling the heavy lifting, while Claude simply calls tools when it needs context. You can even combine multiple approaches in one system, using: - MCP resources for static reference data (for example, API schemas and configuration files) - MCP tools with RAG for searchable documentation - Additional MCP tools for actions like creating tickets or updating records The MCP protocol unifies all these capabilities in one interface. The following projects on GitHub implement these patterns: - [`mcp-crawl4ai-rag`](https://github.com/coleam00/mcp-crawl4ai-rag) - [`rag-memory-mcp`](https://github.com/ttommyth/rag-memory-mcp) ## Conclusion RAG and MCP aren't competing approaches; you can use them to complement each other. RAG provides semantic search capability, and MCP standardizes how your AI connects to external systems, whether those systems perform RAG searches, database queries, or API calls. RAG works best for searching documentation because of its semantic understanding and low token costs. MCP excels at live data queries and actions like creating records and triggering workflows. Modern production AI systems use RAG for searching knowledge bases and MCP for carrying out real-time operations. # react-embeds Source: https://speakeasy.com/blog/react-embeds ### New Features - **React Embeds** - The Speakeasy platform web app is always there if you need it, but if you already have a homegrown platform, you can alternately [add us in as a React embed](/docs/introduction/introduction). Check out the below demo of Alexa, from our engineering team, adding Speakeasy embeds into a web app; (plus [bonus footage](https://www.loom.com/share/ce17b0e28c3746b0aaf704460fdf34a8) of Alexa setting up filtering for the embeds) ### Smaller Changes - **\[Typescript\] Mask sensitive data** - Customers using the typescript SDK can now prevent sensitive fields from entering the platform. [See our docs for how to set up field masking](/docs/introduction/introduction). - **\[Self Hosting\] Support for BigQuery storage** - Store API logs in your data warehouse. User's self-hosting Speakeasy on GCP can now store requests in BigQuery. # rebranding-speakeasy Source: https://speakeasy.com/blog/rebranding-speakeasy Today, we're excited to share Speakeasy's new brand identity: a visual system built to match the precision of our current product and the ambition of our future vision. ## Why rebrand now? Speakeasy isn’t the same company it was a year ago. We started by simplifying SDK generation. Today, we’re powering the next frontier of software: agent-ready APIs. As AI applications like Claude and Cursor reshape software development, APIs need to speak a new language. MCP (Model Context Protocol) has given AI the power to interact with the world via APIs, creating the potential for a tidal wave of productivity and creativity. But this only happens if the ecosystem has APIs ready to handle it. Speakeasy is becoming the MCP company to help every API platform successfully transform into an AI platform. This shift demanded a new visual identity that could: - Communicate our goal to help you "Craft exceptional API experiences" - Give a taste of the refinement and purpose we put into our products - Scale across new products like [Gram](https://getgram.ai), the fastest way to go from API to MCP Server ## Finding our visual voice How do you build a brand that speaks to who we are today and where we want to be tomorrow? We build for engineers and teams who obsess over their craft, and the brand needed to reflect that. We partnered with [Basement Studio](https://basement.studio) to develop what we call "Refined Tech"—a design philosophy that balances technical precision with the warmth of the Speakeasy community. ## A symbol with layers ![Logo construction breakdown](/assets/blog/rebranding-speakeasy/logo-composition.png) The logotype tells our story: stacked layers representing both "the tech stack" and the first letter of our name. ![Full logo](/assets/blog/rebranding-speakeasy/logo.png) ## Evolution, not revolution ![Old logo ASCII pattern](/assets/blog/rebranding-speakeasy/ascii-pattern.gif) It was important to us that we didn't lose the elements of our current brand we loved. We gave the slashes from our old logo a new home. Just about everywhere. We took our original logo, re-imagined it in 3D space, and then ASCII-fied it. Giving us a way to add some retro joy to our site using animations that we felt represented our products—uncompromising quality, going the extra mile and a little bit of fun. ![ASCII logo](/assets/blog/rebranding-speakeasy/ascii-logo.png) ## Bold messaging for a bold mission ![Billboard and poster designs](/assets/blog/rebranding-speakeasy/billboard.png) ![Social media templates](/assets/blog/rebranding-speakeasy/social-media.png) ![Marketing posters](/assets/blog/rebranding-speakeasy/posters.png) Our new brand was a chance to re-evaluate our voice. We're not just another API tool—we're the bridge to the agent economy and our new messaging reflects this. ## Type ![Tobias close up](/assets/blog/rebranding-speakeasy/typography.png) Headlines use Tobias a serif that’s got a bit of retro tech soul. It keeps things human. Supporting content leans on ABC Diatype and Diatype Mono a clean, structured, unmistakably technical font. ![Full type](/assets/blog/rebranding-speakeasy/typography-full.png) ## Color System ![Color palette](/assets/blog/rebranding-speakeasy/rgb-gradient.png) ![Color palette](/assets/blog/rebranding-speakeasy/color-palette.png) Our new palette was born out of utility. It was important that the brand extended naturally into our products. The monochrome foundation gets out of your way when you need to move fast, while the RGB brand gradient was born from the colors we assigned to our SDK generation targets. ## What's next This visual refresh is just the beginning. We’ve already started bringing the brand into the product with Moonshine; our utility-first design system, that we’ll share more about in an upcoming post. We're committed to building APIs that agents love and developers trust. Thank you for being part of our journey. # release-arbitrary-custom-code Source: https://speakeasy.com/blog/release-arbitrary-custom-code Speakeasy generates production-ready SDKs and Terraform providers from OpenAPI specifications, with extensive configuration options through [hooks](/docs/sdks/customize/code/sdk-hooks), [gen.yaml settings](/docs/speakeasy-reference/generation/gen-yaml), and [OpenAPI extensions](https://www.speakeasy.com/docs/speakeasy-reference/extensions). Today, we're introducing persistent edits, a feature that lets you make arbitrary changes anywhere in your generated code which the Speakeasy generator will automatically maintain across regenerations. With persistent edits, you can: 1. Add utility methods, custom properties, and business logic to any generated file 2. Modify configuration files like package.json or pyproject.toml directly 3. Extend your SDK without waiting for Speakeasy to add new configuration options ## The generation ownership problem Speakeasy's code generator has always taken an opinionated approach: the files we generate, we own. This ensures deterministic output and predictable behavior. Given the same configuration, you always get the exact same SDK. This approach has served us well, but it comes with a constraint. When users wanted to add functionality that wasn't supported through our configuration system, they had two options: 1. Wait for us to add a new configuration option 2. Work around it by maintaining changes in separate files Neither option was ideal. Adding configuration for every use case creates complexity, and maintaining changes separately breaks the convenience of having everything in one place. What teams really wanted was simple: make a change to the generated code, and have that change persist. ## How persistent edits work Persistent edits removes the restrictions on where you can make changes to Speakeasy-generated code. The concept is straightforward: modify any file in your generated SDK, and Speakeasy will maintain those changes as long as they don't conflict with updates we're making. The conflict detection works like git merge. If you modify a line and we later regenerate code that touches the same line, you'll see a conflict. But if your changes and ours touch different parts of the file, both changes coexist seamlessly. ### Enabling persistent edits The first time you modify a generated file and run Speakeasy, you'll see a prompt: ```bash ⚠ Modified files detected ╭─────────────────────────────────────────────────────╮ │ We've detected changes to files that Speakeasy │ │ manages. Would you like to enable persistent edits │ │ to maintain these changes? │ │ │ │ Modified files: │ │ - src/sdk/payments.ts │ │ - package.json │ ╰─────────────────────────────────────────────────────╯ Enable persistent edits? (Y/n) ``` Choose yes, and Speakeasy adds `persistentEdits: { enabled: true }` to your gen.yaml. From that point forward, all your changes persist automatically. ## Real-world use cases ### Adding utility methods One of the most common requests we've seen is adding helper methods to generated models. With persistent edits, you can add methods directly to the generated classes: ```typescript filename="src/sdk/models/payment.ts" export class Payment { id: string; amount: number; currency: string; status: PaymentStatus; // Generated code above // Your custom method below /** * Convert payment to invoice format */ toInvoiceItem(): InvoiceItem { return { description: `Payment ${this.id}`, amount: this.amount, currency: this.currency, }; } /** * Check if payment requires customer action */ needsAction(): boolean { return ( this.status === PaymentStatus.RequiresAction || this.status === PaymentStatus.RequiresConfirmation ); } } ``` These methods persist across regenerations. Add a new field to your OpenAPI spec, regenerate, and your custom methods remain intact. ### Modifying configuration files Want to add a dependency that Speakeasy doesn't know about? Just edit package.json directly: ```json filename="package.json" { "name": "@acme/payments-sdk", "version": "1.0.0", "dependencies": { "axios": "^1.6.0", "zod": "^3.22.0", "aws-sdk": "^2.1.0" // Your custom dependency } } ``` Speakeasy maintains this change. When we update other parts of package.json (like version numbers or our dependencies), your aws-sdk entry stays. ### Extending SDK initialization Some teams need custom authentication providers or specialized configuration. Add them directly to the SDK constructor: ```typescript filename="src/sdk/sdk.ts" import { AWSAuth } from "./custom/aws-auth"; export class PaymentsSDK { private client: HTTPClient; private awsAuth?: AWSAuth; constructor(config: SDKConfig) { this.client = new HTTPClient(config); // Your custom initialization if (config.awsAuth) { this.awsAuth = new AWSAuth(config.awsAuth); this.client.interceptors.request.use( this.awsAuth.signRequest.bind(this.awsAuth), ); } } } ``` ## Getting started Persistent edits is available today for all Speakeasy users across SDKs and Terraform providers. To enable persistent edits on an existing SDK: 1. Make a change to any generated file 2. Run `speakeasy run` 3. When prompted, choose **Yes** to enable persistent edits 4. Commit the updated gen.yaml with `persistentEdits: { enabled: true }` For new SDKs, you can enable it immediately by adding the flag to gen.yaml: ```yaml filename=".speakeasy/gen.yaml" configVersion: 2.0.0 generation: sdkClassName: PaymentsSDK maintainOpenAPIOrder: true persistentEdits: enabled: true ``` Once enabled, any modifications you make to generated files will persist across regenerations. The feature works locally and in CI/CD, with no additional setup required. Persistent edits represents our vision for how code generation should work: deterministic and reliable, but flexible enough to meet you where you are. Generated code shouldn't be a black box you work around. It should be a foundation you can build on. --- _Questions about persistent edits or need help with a specific use case? [Book time with our team](/book-demo)_ # release-contract-testing Source: https://speakeasy.com/blog/release-contract-testing import { CodeWithTabs } from "@/mdx/components"; Today we're excited to announce the beta launch of API contract test generation, a powerful new addition to our API development platform that uses your OpenAPI spec to automatically create comprehensive test suites for your API. This release continues our mission of automating the tedious parts of API development, enabling your team to stay focused on building great products. ## The Hidden Cost of API Testing API contract testing is crucial for maintaining reliability and preventing breaking changes, but creating and maintaining test suites is a significant burden on engineering teams. Today, developers face two major challenges: First, they must manually write test code using HTTP frameworks, carefully crafting requests and validation logic for each endpoint. This involves tedious work like handling authentication, managing data structures, and writing assertions. Second, they need to create and maintain realistic test data. While simple endpoints might only need basic examples, real-world APIs often have complex data structures with numerous fields and edge cases. Missing even a single field in your test coverage can lead to broken integrations down the line. The result? Teams either invest significant engineering resources in testing or, more commonly, settle for incomplete test coverage that leaves them vulnerable to breaking changes. ## Generating test suites Speakeasy Contract Testing approaches this problem from both sides. We don't just generate the test code – we also create the test data needed to validate your API's behavior. **Native Test Generation**: Tests are generated in your favorite language's native testing framework ([pytest](https://docs.pytest.org/en/stable/) for Python, [vitest](https://vitest.dev/) for TypeScript, etc.), ensuring they integrate seamlessly with your existing development workflow. We know that debugging impenetrable autogenerated tests is a nightmare, so we've put a lot of work into making the tests we generated look and feel like they were written by your team. { const petstore = new Petstore({ serverURL: process.env["TEST_SERVER_URL"] ?? "http://localhost:18080", httpClient: createTestHTTPClient("createUser"), apiKey: process.env["PETSTORE_API_KEY"] ?? "", }); const result = await petstore.users.create({ id: 10, username: "theUser", firstName: "John", lastName: "James", email: "john@email.com", password: "12345", phone: "12345", userStatus: 1, }); expect(result).toBeDefined(); expect(result).toEqual({ id: 10, username: "theUser", firstName: "John", lastName: "James", email: "john@email.com", password: "12345", phone: "12345", userStatus: 1, }); });`, }, { label: "test_user_sdk.py", language: "python", code: `def test_user_sdk_create_user(): with Petstore( server_url=os.getenv("TEST_SERVER_URL", "http://localhost:18080"), client=create_test_http_client("createUser"), api_key="", ) as s: assert s is not None res = s.user.create_user( request={ "id": 10, "username": "theUser", "first_name": "John", "last_name": "James", "email": "john@email.com", "password": "12345", "phone": "12345", "user_status": 1, } ) assert res is not None assert res == petstore.User( id=10, username="theUser", first_name="John", last_name="James", email="john@email.com", password="12345", phone="12345", user_status=1, )`, }, { label: "user_test.go", language: "go", code: `func TestUser_CreateUser(t *testing.T) { s := petstoresdk.New( petstoresdk.WithServerURL(utils.GetEnv("TEST_SERVER_URL", "http://localhost:18080")), petstoresdk.WithClient(createTestHTTPClient("createUser")), petstoresdk.WithSecurity(""), ) ctx := context.Background() res, err := s.User.CreateUser(ctx, &components.User{ ID: petstoresdk.Int64(10), Username: petstoresdk.String("theUser"), FirstName: petstoresdk.String("John"), LastName: petstoresdk.String("James"), Email: petstoresdk.String("john@email.com"), Password: petstoresdk.String("12345"), Phone: petstoresdk.String("12345"), UserStatus: petstoresdk.Int(1), }) require.NoError(t, err) assert.Equal(t, 200, res.HTTPMeta.Response.StatusCode) assert.NotNil(t, res.User) assert.Equal(t, &components.User{ ID: petstoresdk.Int64(10), Username: petstoresdk.String("theUser"), FirstName: petstoresdk.String("John"), LastName: petstoresdk.String("James"), Email: petstoresdk.String("john@email.com"), Password: petstoresdk.String("12345"), Phone: petstoresdk.String("12345"), UserStatus: petstoresdk.Int(1), }, res.User) }`, } ]} /> ## Solving the testing workflow Generating testing code is only part of the solution. Testing is a workflow, and we know every team's testing needs are different, so we've built flexibility and out of the box completeness into the core of our testing platform: **Comprehensive Data Coverage**: Beyond just testing happy paths, our platform generates test data that covers edge cases and optional fields. If you have examples in your OpenAPI spec, we'll use those. If not, we'll generate realistic mock data that validates your API's full schema. **Flexible testing environment**: Have your own sandbox environment? We've got you covered. You can run your tests against any server URL you'd like. Don't have a sandbox? We've got you covered there too. Our platform will create a mock server for you to run your tests against. **Flexible Configuration**: We are ready to support your preferred workflow. For teams managing their OpenAPI spec, simply add the `x-speakeasy-test` annotation to your OpenAPI spec to enable testing for specific endpoints or your entire API. Alternatively, you can use our external configuration support to define tests without modifying the underlying OpenAPI document. **CI/CD Integration**: Tests can be run locally during development or automated in your CI/CD pipeline. We provide GitHub Actions workflows out of the box, making it easy to validate API changes on every pull request. ## Testing end-to-end In addition to contract testing, we're also rolling out the beginnings of end-to-end testing support. This will allow you to validate your API's behavior across multiple endpoints, ensuring that your API behaves as expected in complex workflows. For example, you might want to test a workflow where a user logs in, creates an order, and then verifies the order appears in their order history. This kind of multi-step testing is crucial because it validates not just individual endpoints, but the connections between them. To enable end-to-end testing, we're leveraging the [Arazzo specification](/openapi/arazzo), which allows you to arrange API calls into multi-step workflows. The Arazzo specification is fully compatible with the OpenAPI specification, so you can use your existing OpenAPI documents to generate end-to-end tests with a fully open source framework. We'll be rolling out more support for bootstrapping Arazzo workflows in the coming months, so stay tuned for more updates! ## Getting Started Contract Testing is available for early access. You can generate tests in TypeScript, Python, and Go today, with more languages (Java, C#, etc.) coming soon. Existing customers can reach out to their account manager to get access. If you're new to Speakeasy, you can request access to the testing module by filling out this [form](/book-demo). We're excited to see how this helps teams build more reliable APIs with less effort. This is just the beginning – we have lots more planned for testing, including deeper workflow validation, end-to-end testing support, and enhanced mock data generation. Ready to automate your API testing? [Sign up now](https://app.speakeasy.com/) or check out our [documentation](https://speakeasy.com/docs) to learn more. # release-custom-code-regions Source: https://speakeasy.com/blog/release-custom-code-regions import { Table } from "@/mdx/components"; We're excited to announce **Custom Code Regions**, a powerful new feature that puts you in complete control of your SDKs. Whether you need to add custom methods, integrate third-party libraries, or build bespoke functionality, Custom Code Regions let you do it all—directly in the generated SDK. And the best part? Your customizations persist across updates, with no need to edit your OpenAPI spec or deal with workarounds. --- ### What are Custom Code Regions? Custom Code Regions give developers the ability to embed their own code into specific parts of the generated SDK. Using foldable region syntax, you can define areas in the codebase for your custom logic. These regions are preserved during regenerations, meaning you can enhance the SDK while maintaining its stability. Whether you're adding helper methods, extending SDK functionality, or introducing custom integrations, Custom Code Regions make it easy to personalize your SDK without impacting the underlying OpenAPI spec. #### **How It Works** Custom Code Regions leverage foldable region syntax, inspired by Visual Studio Code, to define and preserve custom code sections. Here's an example: ```typescript import { ClientSDK } from "./lib/sdks.js"; // #region imports import chalk from 'chalk'; // #endregion imports class Acme extends ClientSDK { // ... generated code ... // #region sdk-class-body greet(name: string): string { return chalk.green(`Hello, ${name}!`); } // #endregion sdk-class-body } ``` During SDK generation, the generator identifies these regions and ensures your custom code is seamlessly integrated into the updated SDK. This process ensures consistent formatting while preserving customizations. ### Why Custom Code Regions? For developers, flexibility is key. Before Custom Code Regions, SDK customization options were limited to overlays and hooks, which are powerful tools but sometimes require modifying the OpenAPI spec or working programmatically within the generation process. Custom Code Regions provide a simpler, more direct approach: 1. **Direct Control**: Write custom logic right in the SDK, bypassing spec modifications or hook configurations. 2. **Persistent Changes**: Customizations are preserved during SDK regenerations, so you don't have to worry about losing your code when the spec changes. 3. **Unlimited Flexibility**: From helper methods to advanced integrations, you can do anything with Custom Code Regions. --- ### How Custom Code Regions Fit with Overlays and Hooks Custom Code Regions complement overlays and hooks, giving developers three levels of control over their SDKs:
By combining these tools, you can build SDKs that are perfectly tailored to your needs. --- ### Ready to Try Custom Code Regions? Custom Code Regions are now available for enterprise customers in TypeScript and Python SDKs generated with the latest Speakeasy CLI. Support for additional languages is on the way, so stay tuned. > 📚 Dive into the documentation: > 🔗 [Custom Code Regions Documentation](/docs/customize/code/code-regions/overview) # release-gram-beta Source: https://speakeasy.com/blog/release-gram-beta import { YouTube, Callout } from "@/lib/mdx/components"; import { CalloutCta } from "@/components/callout-cta"; import { GithubIcon } from "@/assets/svg/social/github"; **[Gram](https://app.getgram.ai) is an [open source](https://github.com/speakeasy-api/gram) platform for building MCP servers is now in public beta**. Instead of exposing raw APIs that confuse agents, Gram helps you curate tools, add context, and compose custom tools that represent complete business operations. [Sign up today](https://app.getgram.ai/) and start building MCP servers that perform. } title="Gram OSS Repository" description="Check out Github to see how it works under the hood, contribute improvements, or adapt it for your own use cases. Give us a star!" buttonText="View on GitHub" buttonHref="https://github.com/speakeasy-api/gram" /> ## MCP, MCP everywhere, nor any tool to use In just a year, MCP has gone from protocol white paper to critical infrastructure. As developers scramble to get a handle on this new technology, **MCP servers are everywhere, and yet, good MCP servers remain elusive**. Any developer can create a basic MCP server in minutes. The code isn’t complicated and the infra is straightforward. But building a server that AI agents can actually use effectively is a nuanced, ever-evolving challenge. Most MCP servers fail for one of four reasons: 1. They expose too many tools, creating decision paralysis and tool confusion for LLMs. 2. Tools lack the rich descriptions and examples that help LLMs understand when and why to use each tool. 3. MCP servers provide CRUD-based tools lacking context, which forces LLMs and agents to construct workflows on the fly. 4. They have an incomplete authentication story preventing production usage. The result? Servers that may "technically" be MCP-compliant... but in practice, confuse agents and produce unreliable results. Ultimately, the promise of MCP and agents goes unfulfilled, and dev teams give up. ## Gram: build tools that perform Gram is an open source platform that bridges the gap between tools and AI agents. Our unique focus is tool design. We help you curate and compose your APIs into intelligent custom-built tools that agents can actually use effectively. Most frameworks for building MCP servers focus on the mechanics of server creation: decorators, custom functions, and infrastructure. While these are important foundational elements, the critical factor for agent success is tool design: how you structure, describe, and organize the capabilities your server exposes. LLMs don't interact with your code, they interact with your tool definitions, descriptions, and prompts. **Gram enables teams to add context, refine prompts, and compose custom tools until your APIs can be consumed effectively by LLMs.** Here’s how it works. ### 1. **Curate Toolsets** → Eliminate AI Confusion When you upload your API to Gram, we convert your API endpoints into primitive tools. We help you identify which capabilities actually matter for your use cases and filter out the noise. A 600-endpoint API becomes a focused 5-30 tool collection that AI agents can navigate confidently. All your tools are grouped into toolsets. You can remix tools across different APIs into a single toolset ensuring you have all the right tools for a specific use case rather than a single API. ### 2. **Add Context** → Improve AI Performance APIs designed for developers often lack the rich context that AI agents need. Our platform lets you enhance tool descriptions, add business logic, and provide examples that help LLMs understand not just _what_ each tool does, but _when_ and _why_ to use it. Every time you update a tool, immediately test it out in a convenient playground. ### 3. **Define Custom Tools** → Create Complete Solutions This is where Gram truly differentiates itself. Instead of forcing AI agents to orchestrate multiple tool calls together manually, you can define what we call "custom tools," tools that chain together smaller atomic tools and represent specific business operations. For example, let's say you ask your AI a question like "Summarize ACME Corp's health". Without custom tools, your AI might need to figure out, from a plethora of tools available, that it needs to (a) find the ACME Corp customer record and id; (b) search for the id within the CSAT scores table; (c) search for ACME Corp using a specific tool from your CRM; (d) retrieve relevant notes within your CRM; (e) summarize all the above. As the number of tool calls that the AI needs to independently figure out increases, the chance of making an error increases: e.g. if there's a 5% chance of each individual call failing i.e. 95% chance of success, then across five calls that figure drops to (1-5%)^5 = 77% chance of success. With custom tools, you can instead create a tool that is targeted at a specific use case, and that calls specific tools that you define — decreasing the chance that AI encounters tool confusion. ## Prototype fast, scale faster Getting started with Gram takes under a minute, but our platform is built to grow into a complete MCP control plane that enterprises need. **OAuth 2.1 Compliance**: MCP servers deployed through Gram can optionally include our OAuth 2.1 proxy with Dynamic Client Registration support, and PKCE flows. Your tools are secure by default, not as an afterthought. Already have your own OAuth flow implemented? Bring your own OAuth authorization server and add it in front of any Gram hosted MCP Server with just a few clicks. **Centralized Management**: You get unified control across all the agent tools within your org, with role-based access control, comprehensive audit logging, and compliance reporting. It's the difference between scattered tools and a strategic tool repository. **Production Infrastructure**: Our hosted infrastructure means automatic scaling, uptime SLAs, zero downtime deployments, and 24/7 monitoring. No local setup, no infrastructure management, no maintenance overhead. **Every toolset in Gram is immediately available as a hosted MCP server.** Whether your teams use Claude Desktop, ChatGPT, Cursor, or any other MCP-compatible platform, integration is instant and tested for different AI ecosystems. ## Real Impact: From Prototype to Production MCP servers have been transformational for the companies we work with. We see three primary use cases driving adoption: ### 1. **Launching Public MCP Servers** Companies are exposing their APIs as MCP servers to make their platforms AI-native. By creating well-designed MCP servers, businesses enable AI agents to seamlessly integrate with their services, opening up new distribution channels and user experiences. Instead of users needing to learn complex APIs or navigate web interfaces, they can simply ask an AI agent to perform tasks using natural language. We want to make launching `mcp.yourcompany.com` as easy as possible. ### 2. **Embedding AI-Native Experiences** Product teams are embedding AI capabilities directly into their applications using MCP servers as the integration layer. All major agentic frameworks like Langchain, OpenAI Agents SDK, and PydanticAI support MCP servers for accessing tools. MCP servers interact with their existing APIs and data sources allowing developers to embed AI chat, image, and video generation, and audio synthesis into their applications. This approach dramatically reduces development time while providing users with intuitive, natural language interfaces. ### 3. **Internal Workflow Orchestration** Every company has internal admin APIs for operational capabilities: user management, billing operations, system diagnostics, data analytics, configuration controls, etc. Teams are replacing internal dashboards, saved SQL commands, and shared bash scripts with AI agents powered by MCP servers. Complex multi-step operations that previously required navigating multiple systems can now be accomplished with a simple natural language request. ## Why we open sourced Gram We made the decision to [open source Gram](https://github.com/speakeasy-api/gram) because the MCP ecosystem is evolving rapidly, and we realized that much of what we're building will continue to change in the coming months. MCP has already transformed from a local-only tool to supporting remote servers, and this pace of innovation shows no signs of slowing. With an open source approach, we can engage directly with the community as we build this new piece of the AI engineering stack. This collaboration is essential for integrating with other agentic frameworks and ensuring Gram works seamlessly across the broader ecosystem. Most importantly, Gram is infrastructure that we're asking companies to rely on. Being able to see the code, understand how we build it, and evaluate our engineering practices gives users confidence in what we're doing. Transparency builds trust, especially for critical infrastructure. The tech stack includes: - [TypeScript](https://www.typescriptlang.org/) – dashboard language. - [Golang](https://go.dev/) - backend language - [Goa](https://github.com/goadesign/goa) - design-first API framework in Golang. - [Temporal](https://temporal.io/) - workflow engine. - [Polar](https://github.com/polarsource/polar) - usage-based billing. - [Speakeasy](https://www.speakeasy.com/) - generated SDKs. - [Our own homegrown OpenAPI parser](https://github.com/speakeasy-api/openapi) **Contributing**: The entire platform is open source with a comprehensive development guide. Getting started locally requires just running a mise script, with only two external dependencies: a Temporal key and OpenRouter key. We're particularly looking for contributions around MCP server capabilities, expanding our OpenAPI parser to support more API shapes, and implementing newer features from the MCP specification. ## Getting Started As of today, Gram is in **public beta** and anyone can sign up and get started. Pricing is usage-based with a generous free tier (1K tool calls) to encourage teams to experiment and iterate quickly. The future belongs to organizations that make their capabilities easily accessible to AI agents. The question isn't whether your company will need MCP—it's whether you'll lead the transformation or follow it. **Try it out today** [Join Gram](https://app.getgram.ai/) and start powering AI agents by leveraging your APIs. ## What’s coming Looking ahead, we're expanding the platform in three key directions: **Gram Functions** - Moving beyond OpenAPI-only tool creation, teams will soon be able to upload custom TypeScript and Python code to create sophisticated agent tools that don't require existing APIs. This opens Gram to any team building AI workflows, not just those with existing OpenAPI specifications. **MCP server import** - Rather than building yet another marketplace, we're enabling teams to import and manage the best MCP servers from across the ecosystem—including official servers from Anthropic's registry, official first party servers from the likes of Notion, Linear etc. and third-party marketplaces like Smithery. Your Gram workspace becomes a unified control plane for all your organization's MCP servers whether you're making a server or consuming an external one. **Self-hosted data plane** - Enterprise teams will gain self-hosted data plane options to keep API traffic within their VPC, comprehensive observability and audit trails, role-based access controls, and compliance certifications including SOC2 Type 2. Plus, embeddable Gram Elements will let you add curated chat experiences directly into your applications. Check out the complete roadmap [here](https://roadmap.speakeasy.com/roadmap). --- _Interested in learning more about our approach to MCP and enterprise AI integration? [Book time with our team](/book-demo)_ # release-gram-functions Source: https://speakeasy.com/blog/release-gram-functions import { CalloutCta } from "@/components/callout-cta"; import { GithubIcon } from "@/assets/svg/social/github"; import { YouTube } from "@/lib/mdx/components"; Gram is the MCP cloud platform that makes building production-ready MCP servers fast and easy. Today, we're making it even more powerful with [Gram Functions](/docs/gram/getting-started/typescript), a TypeScript framework that lets you define agent tools in code without worrying about MCP protocol internals or managing infrastructure. Write your tool logic, deploy to Gram's managed platform, and get production-grade MCP servers with enterprise performance, observability, and security built-in. With Gram Functions, you can: 1. Define agent tools using an intuitive TypeScript framework that abstracts away protocol complexity 2. Deploy to managed infrastructure with one command 3. Scale production MCP servers without operational overhead ## Moving beyond APIs In the 45 days since Gram launched, we've seen teams like Polar use Gram-hosted MCP servers to build [amazing AI experiences](/customers/polar). But we always knew that API-based MCP was only the beginning of the story. Sometimes a company's APIs are the right abstraction for an LLM to use, but often they're not. The right API endpoints may not be present, or they don't map cleanly to business operations, or, the LLM simply needs capabilities that go beyond HTTP (querying a DB). Today, we're taking care of all those use cases with **Gram Functions**. ## An agent tool framework built for TypeScript developers } title="Gram OSS Repository" description="Check out Github to see how it works under the hood, contribute improvements, or adapt it for your own use cases. Give us a star!" buttonText="View on GitHub" buttonHref="https://github.com/speakeasy-api/gram" /> Gram Functions lets you compose TypeScript tools in code that are hosted as MCP servers. No need for OpenAPI. No need for detailed MCP protocol knowledge. No infrastructure to manage. Just define tools in code and deploy them securely as MCP servers in Gram's serverless environment. Remix tools across different functions and OpenAPI into a single MCP server. The Gram Functions API is designed to be intuitive and familiar to TypeScript developers. Each tool definition has four simple components: - **`name`**: A unique identifier for the tool that agents will use to call it - **`description`**: A human-readable explanation that helps agents understand when to use the tool - **`inputSchema`**: A Zod schema defining the expected parameters with type safety and validation - **`execute`**: An async function containing your business logic ```typescript filename="functions.ts" import { Gram } from "@gram-ai/functions"; import * as z from "zod/mini"; const gram = new Gram().tool({ name: "greet", description: "Greet someone special", inputSchema: { name: z.string() }, async execute(ctx, input) { return ctx.text(`Hello, ${input.name}!`); }, }); ``` That's it. Upload this code to Gram, and it immediately becomes a tool that agents can use. Once uploaded, a tool can be used across multiple MCP servers. ### A focused approach to MCP Gram Functions isn't trying to be an exhaustive implementation of the MCP protocol. Instead, we're focused on making the core building blocks of MCP like tools and resources, as ergonomic as possible to work with through a high level API. The tools you build with Gram are exposed as MCP servers and work with any MCP client, but they represent a carefully selected subset of what's possible with the protocol. This tradeoff lets us deliver a dramatically better developer experience for the most common use cases while maintaining full compatibility with the MCP ecosystem. For those who want to start with existing implementations we do support a complete pass through of natively implemented servers using the MCP SDK. ## Functions in production The above example was basic, but Gram Functions can handle complex workflows that call multiple APIs, query databases, and perform business logic: ```typescript filename="investigate_failed_payment.ts" export const gram = new Gram().tool({ name: "investigate_failed_payment", description: "Investigate why a payment failed and check customer history", inputSchema: { paymentId: z.string(), }, async execute(ctx, input) { // Get payment details const payment = await fetchPaymentDetails(input.paymentId, ctx); // Query customer's payment history const history = await fetchPaymentHistory(payment.customerId, ctx); // Check if card is about to expire const cardStatus = await checkCardExpiry(payment.cardId, ctx); // Query risk score const riskScore = await getRiskScore(payment.customerId, ctx); return ctx.json({ failureReason: payment.errorMessage, customerHistory: { totalPayments: history.total, failureRate: history.failureRate, lastSuccessful: history.lastSuccess, }, cardExpiring: cardStatus.expiresWithin30Days, riskLevel: riskScore.level, recommendedAction: determineAction(payment, history, riskScore), }); }, }); ``` Without a Gram function, an agent would need to make four separate API calls, construct each request correctly, parse responses, and synthesize the results. Each additional call multiplies the chance of failure. If each call has a 95% success rate, four calls drops overall success to 81%. Gram Functions lets you encode this workflow once, correctly, and agents simply call `investigate_failed_payment`. ## Getting started Functions are available to users on every tier (including free). You can create a new Gram Functions project to start writing tools: ```bash filename="Create Gram project" npm create @gram-ai/function > npx > "create-function" │ ◇ Pick a framework │ Gram │ ◇ What do you want to call your project? │ demo-mcp │ ◇ Where do you want to create it? │ demo-mcp │ ◇ Initialize a git repository? │ Yes │ ◇ Install dependencies with npm? │ Yes │ │ ◆ All done! Jump in with cd demo-mcp. │ │ Some next steps: │ │ - Start a local development MCP server with npm run dev │ │ - Build your function with npm run build │ │ - Create MCP servers from your tools in the Gram dashboard: https://app.getgram.ai │ │ Have fun 🚀 ``` This creates a project with example tools and our minimal framework: ```typescript filename="src/gram.ts" import { Gram } from "@gram-ai/functions"; import * as z from "zod/mini"; export const gram = new Gram().tool({ name: "greet", description: "Greet someone special", inputSchema: { name: z.string(), }, async execute(ctx, input) { return ctx.json({ message: `Hello, ${input.name}!` }); }, }); ``` Deploy to Gram: ```bash filename="deploy" npm run build npm run push ``` Your function is now live as an MCP tool, available at your Gram-hosted MCP server URL. Connect it to Claude Desktop, Cursor, or any MCP-compatible platform. ## What's next We're launching our TypeScript framework today and will be expanding its capabilities in the coming months Looking ahead: - **Support for ChatGPT Apps** - providing a high level widget API in the functions framework for building ChatGPT apps. - **Ability to stage and draft toolsets** - add the ability to preview changes to your tools before they're deployed into production We're building Gram Functions to be the fastest path from code to production MCP. Whether you're wrapping internal APIs, orchestrating complex workflows, or building entirely new capabilities, Gram Functions provides the infrastructure you need without the complexity you don't. **Try it today**: [Sign up for Gram](https://app.getgram.ai/) and deploy your first function. --- _Questions about Gram Functions or need help getting started? [Book time with our team](/book-demo)_ # release-gram-private-server-oauth Source: https://speakeasy.com/blog/release-gram-private-server-oauth As AI agents become standard tools within organizations, platform teams face a new challenge: how do you give everyone access to the same MCP servers without managing credentials for each person? The answer increasingly looks like a centralized MCP gateway where one team manages servers, environments, and API keys while everyone else just connects and works. Gram's private servers already solve the hosting side of this problem. Today, we're adding the last piece: Gram OAuth lets your team members authenticate with your org's MCP servers using standard auth methods like Google SSO instead of needing to provision & manage API keys. ## The MCP gateway pattern When an organization adopts MCP, the typical path starts with individual developers running their own servers. This works fine at small scale, but creates problems as usage grows: duplicated credentials across machines, no visibility into what's being accessed, and no way to revoke access when someone leaves. MCP platforms like Gram offer a centralized alternative. A platform team hosts all the org's MCP servers on Gram, configures the credentials once, and shares access with everyone who needs it. Users connect to the shared servers without needing to manage API keys themselves. ## How Gram OAuth works When a user connects to a private MCP server with Gram OAuth enabled: 1. They're prompted to sign in to Gram 2. They authenticate with Google, email, or their preferred method 3. Gram issues OAuth tokens automatically via the standard DCR flow 4. They gain access to the server without ever seeing an API key The experience is identical to signing in to any other service. For administrators, onboarding is just adding someone to the organization. ## Getting started To enable Gram OAuth on a private server: 1. Navigate to your private MCP server in the Gram dashboard 2. Enable **Gram OAuth** in the authentication settings 3. Add team members to your Gram organization 4. Share the server connection details with your team For detailed configuration options, see the [private server authentication documentation](https://www.getgram.ai/docs/host-mcp/public-private-servers#gram-oauth). --- _Questions about Gram OAuth or need help setting up private servers for your team? [Book time with our team](/book-demo)_ # Java SDKs, now async‑native: `CompletableFuture` + Reactive Streams Source: https://speakeasy.com/blog/release-java-async import { Callout, Table } from "@/lib/mdx/components"; Java's ecosystem has steadily shifted toward asynchronous, event‑driven programming. **Reactive architectures are increasingly adopted by major platforms**[^1], while `CompletableFuture` is ubiquitous in modern APIs. SDKs should match this **async‑first** reality—without forcing teams to abandon familiar synchronous code. ## What's new * **Async native**: `CompletableFuture` returned by standard methods. * **Streaming built‑in**: Reactive Streams `Publisher` for pagination, SSE, JSONL, and file I/O. * **Dual SDK**: Synchronous by default; opt in with `.async()` per call‑site or service. * **Blob**: A light, framework‑agnostic byte‑stream abstraction for efficient uploads/downloads. ## Why async Traditional blocking SDKs dedicate **one thread per in‑flight request**. That results in idle threads, wasted memory, and hard‑to‑tune pools. Especially under high concurrency and variable network latency. Async I/O scales with a small, fixed number of threads without sacrificing composition[^3] or [backpressure](https://www.reactivemanifesto.org/glossary#Back-Pressure)[^2]. For key reactive concepts, see the [Reactive Manifesto glossary](https://www.reactivemanifesto.org/glossary). Python has `asyncio`, JavaScript has async/await with `Promise`, Go has goroutines, and Swift has async/await. **Async I/O is the de facto model** across modern languages. Java's ecosystem is rapidly moving in this direction—think of this as **Python's `asyncio` but for Java**, with the added benefits of strong typing and mature reactive ecosystems. ### When async provides the most value
Your developers have diverse needs: some build high-concurrency microservices that benefit from async patterns, while others prefer straightforward synchronous flows. **Don't make them choose at the SDK level**—cater to both paradigms in one unified SDK so they can adopt async where it adds value without abandoning familiar synchronous code. The most successful SDKs optimize for **developer choice**, not architectural dogma. Here's how this works in practice. We expose both sync and async interfaces from **one** SDK: ```java // Build once — defaults to synchronous behavior TradingSDK sdk = TradingSDK.builder() .serverURL("https://api.trading.com") .apiKey("your-api-key") .build(); // Synchronous usage (existing behavior) Portfolio portfolio = sdk.getPortfolio("user-123"); List trades = sdk.getTrades(portfolio.getId()); // Asynchronous usage via .async() CompletableFuture> asyncTrades = sdk .async() .getPortfolio("user-123") .thenCompose(p -> sdk.async().getTrades(p.getId())); ``` The builder yields a synchronous SDK by default, so **no breaking changes**. Opt into async via `.async()`; the mode applies consistently across sub‑SDKs. ## Implementation deep‑dive Our HTTP stack uses Java 11's `HttpClient` async APIs and [NIO.2](https://docs.oracle.com/javase/tutorial/essential/io/fileio.html) primitives ([`ByteBuffer`](https://docs.oracle.com/en/java/javase/11/docs/api/java.base/java/nio/ByteBuffer.html), [`AsynchronousFileChannel`](https://docs.oracle.com/en/java/javase/11/docs/api/java.base/java/nio/channels/AsynchronousFileChannel.html), [`AsynchronousSocketChannel`](https://docs.oracle.com/en/java/javase/11/docs/api/java.base/java/nio/channels/AsynchronousSocketChannel.html)) for end‑to‑end non‑blocking I/O. ```java // The underlying HTTP client call client.sendAsync( request, HttpResponse.BodyHandlers.ofPublisher()); // returns a CompletableFuture ``` Our SDKs go to lengths to ensure there is as little as possible thread blocking work. For standard JSON responses, we collect and decode bytes asynchronously using `thenApply/thenCompose`. For streams, we hook into **`Flow.Publisher>`**. ### Async iterables via Reactive Streams For async iterables (pagination, streaming responses), we represent them as **[Reactive Streams](https://www.reactive-streams.org/)** [`Publisher`](https://github.com/reactive-streams/reactive-streams-jvm/blob/v1.0.4/README.md#1-publisher-code). Traditional Java collections or custom iterators aren't feasible for async scenarios—they can't express **non‑blocking [backpressure](https://www.reactivemanifesto.org/glossary#Back-Pressure)** or handle variable consumption rates gracefully. Using `Publisher` is an increasingly common idiom in modern Java applications, providing seamless interoperability with mature reactive ecosystems like [Project Reactor](https://projectreactor.io/), [RxJava](https://github.com/ReactiveX/RxJava), [Akka Streams](https://doc.akka.io/libraries/akka-core/current/stream/index.html), [Vert.x](https://vertx.io/docs/vertx-reactive-streams/java/), and [Quarkus Mutiny](https://quarkus.io/guides/mutiny-primer). We keep dependencies light by implementing JDK‑native operators (map, mapAsync, concat, wrap, flatten) through custom publishers, subscribers, and subscriptions. ### Protocol‑aware streaming * **SSE & JSONL**: Custom publishers bridge raw byte streams to protocol parsers, yielding typed events on backpressure‑aware `Publisher` * **Pagination**: A generic, non‑blocking pagination publisher drives page fetches using pluggable `ProgressTrackerStrategy` to parse cursors and termination conditions ### Retries, timeouts, and hooks * **Async retries** use `ScheduledExecutorService` for non‑blocking exponential backoff with jitter * **Timeouts/cancellation[^5]** are surfaced with `CompletableFuture#orTimeout`, `completeExceptionally`, and cancellation propagation to HTTP requests * **Hook transformations** leverage `CompletableFuture` for zero‑blocking customization points ## Efficient payloads with `Blob` `Blob` is our core abstraction for working with streams of `ByteBuffer`: ```java // Factories Blob.from(Paths.get("large-dataset.json")); // File path Blob.from(inputStream); // InputStream Blob.from("text content"); // String Blob.from(byteArray); // byte[] Blob.from(flowPublisherOfByteBufferLists); // Flow.Publisher> // Consumption blob.asPublisher(); // org.reactivestreams.Publisher blob.toByteArray(); // CompletableFuture blob.toFile(targetPath); // CompletableFuture blob.toInputStream(); // InputStream bridge ``` **Multipart uploads** concatenate publishers—each part (form field or file) is its own stream. **Downloads** expose `Blob` so you can stream to disk without buffering whole payloads. Enable `enableStreamingUploads: true` to get the same memory-efficient upload capabilities ```java // Sync SDK with streaming uploads syncSDK.uploadDataset(Blob.from( Paths.get("./datasets/large-file.jsonl"))); ``` ```java // Stream a large model to disk CompletableFuture model = asyncMLSDK.downloadModel("bert-large.bin") .thenCompose(res -> res.body().toFile(Paths.get("./models/bert-large.bin"))); // Stream binary telemetry (JSONL/SSE similar) var bytes = asyncIoTSDK.streamSensorData("factory-floor-1") .thenApply(res -> res.body().asPublisher()); // Use your favourite reactive libraries to react to stream Flux.from(Mono.fromFuture(bytes)) .flatMap(Flux::from) .buffer(1024) .map(this::parseBinaryTelemetry) .filter(r -> r.getTemperature() > CRITICAL_THRESHOLD) .subscribe(this::triggerAlert); // Upload a 10GB dataset without memory pressure CompletableFuture up = asyncDataSDK.uploadDataset( Blob.from(Paths.get("./datasets/customer-data-10gb.jsonl")) ); ``` We expose **Reactive Streams** types in the public API for ergonomic use with the wider ecosystem. Java 11+ HTTP APIs expose `Flow.Publisher` under the hood; we convert via [`FlowAdapters`](https://www.reactive-streams.org/reactive-streams-jvm-1.0.4-javadoc/org/reactivestreams/FlowAdapters.html) and flatten `List` as needed. If you require `Flow.Publisher`, adapters are available in both directions. ## Reactive streaming patterns ### Reactor ```java // Paginate users Publisher users = asyncCRMSDK.listUsers().asPublisher(); Flux.from(users) .filter(User::isActive) .flatMap(u -> sendWelcomeEmail(u).thenReturn(u)) .subscribe(); ``` ### RxJava ```java // Real-time logs (SSE/JSONL abstracted) Publisher logStream = asyncLoggingSDK.streamLogs(); Flowable.fromPublisher(logStream) .filter(log -> "ERROR".equals(log.getLevel())) .window(30, TimeUnit.SECONDS) .flatMapSingle(window -> window.toList()) .subscribe(alertingService::sendAlert); ``` ### Akka Streams ```java // Batched notifications Publisher notifications = asyncNotificationSDK.streamNotifications(); Source.fromPublisher(notifications) .filter(n -> "HIGH".equals(n.getPriority())) .mapAsync(10, this::enrichWithUserData) .grouped(50) .runForeach(batch -> pushNotificationService.sendBulk(batch), system); ``` ## On Virtual Threads [Virtual threads](https://docs.oracle.com/en/java/javase/21/core/virtual-threads.html) make **synchronous** code scale far better by reducing the cost of blocking. They're a great fit for simple request/response flows and can be used with the synchronous SDK today. Our **async SDK** still adds value where you need **backpressure, streaming, cancellation, and composition across multiple concurrent I/O operations**—patterns that `CompletableFuture`/`Publisher` express naturally. ## For API Producers: the unified paradigm advantage * **Broader reach**: Works for Spring MVC (sync) *and* WebFlux/Quarkus (async) * **Migration flexibility**: Adopt async gradually without replacing integrations * **Lower maintenance**: One codebase; two clean interfaces * **Consistent DX**: Same auth, errors, and config everywhere ## Getting started Enable the following flag in your `.speakeasy/gen.yaml`: ```yaml java: asyncMode: enabled ``` Then run `speakeasy run` to regenerate your SDK. For existing SDKs, see our [migration guide](https://www.speakeasy.com/docs/customize/java/java-async-migration) for step-by-step instructions on enabling async support for your SDKs. ## Looking ahead This release lays the groundwork for the full spectrum of modern Java patterns—from cloud‑native microservices to real‑time streaming systems. Ready to unlock the benefits of non‑blocking I/O? **[Regenerate your Java SDK](https://speakeasy.com/docs/sdks/create-client-sdks)** and try it today. --- [^1]: [Netflix API with RxJava](https://netflixtechblog.com/reactive-programming-in-the-netflix-api-with-rxjava-7811c3a1496a) | [The Rise of Reactive Programming in Java](https://web.archive.org/web/20241213112122/https://www.icertglobal.com/the-rise-of-reactive-programming-in-java-blog/detail) | [Java Reactive Programming: An In-Depth Analysis](https://www.rethink.de/en/blog/java-reactive-programming-an-in-depth-analysis) | [Reactive Programming is Not a Trend](https://www.thisdot.co/blog/reactive-programming-is-not-a-trend-why-the-time-to-adopt-is-now) | [Java Reactive Programming](https://sam-solutions.com/blog/java-reactive-programming/) [^2]: [Spring WebFlux Overview](https://docs.spring.io/spring-framework/reference/web/webflux/new-framework.html) — Why reactive/non‑blocking scales with a small, fixed number of threads. [^3]: **Composition** — The ability to chain and combine async operations declaratively, like `future.thenCompose()` or stream operators, without manual thread coordination. [^4]: **Streaming** — Processing data as a continuous flow of elements rather than loading everything into memory at once, enabling efficient handling of large datasets or real-time data. [^5]: **Cancellation** — The ability to interrupt or stop async operations gracefully, propagating cancellation signals through composed operations to free resources and avoid unnecessary work. # SDK generated by Speakeasy handles all streaming complexity Source: https://speakeasy.com/blog/release-jsonl-support Structured Streaming APIs powered by formats like JSON Lines (JSONL) are revolutionizing how applications consume real-time data. Their surging popularity isn't surprising—they enable everything from token-by-token LLM interactions to large-scale analytics with minimal latency, fundamentally changing how AI systems process information. Today, we're excited to announce built-in support for JSONL in Speakeasy-generated SDKs—eliminating complex boilerplate code and improving developer experience. This capability also complements our existing support for the [Server-Sent Events (SSE)](/docs/customize/runtime/server-sent-events) streaming protocol. ## What is JSON Lines (JSONL)? JSON Lines is a simple yet powerful data format designed specifically for streaming and incremental processing scenarios. By structuring data in a consistent line-by-line approach, JSONL offers significant advantages for modern API implementations: * **One JSON per line:** Each line is a complete, standalone JSON object * **Newline separated:** Individual objects separated by `\n` characters * **Streaming-friendly:** Process data incrementally as it arrives * **Memory efficient:** Handle large datasets without significant memory overhead * **Easily appendable:** Perfect for log aggregation and real-time event streams Here's a simple JSONL stream example: ```jsonl {"id": 101, "username": "alice", "status": "active"} {"id": 102, "username": "bob", "status": "inactive", "groups": ["dev", "test"]} {"id": 103, "username": "charlie", "status": "active", "last_login": "2025-04-07T10:00:00Z"} ``` Think of JSONL like receiving a continuous stream of small packages. Each package can be opened and processed immediately, unlike traditional JSON which requires receiving the complete payload first. ### Why JSONL matters for AI & real-time APIs JSONL powers key capabilities your API consumers increasingly demand: * **Enhanced AI/LLM experiences:** Token-by-token streaming vastly improves perceived performance * **Reactive AI agents:** Enables real-time analysis and mid-stream interactions * **Large dataset processing:** Efficiently handle large datasets with minimal memory footprint * **Real-time data processing:** Essential for live dashboards and monitoring tools ## The Challenge: Consuming JSONL streams When developers interact with JSONL-formatted API responses, they face significant technical hurdles. Consuming these streams requires writing extensive custom code to handle asynchronous connections, buffer incoming data, parse JSON objects line by line, validate against expected schemas, and transform raw data into usable objects. This complexity creates a substantial barrier to entry, especially for developers who simply want to integrate with your API quickly and reliably. Each implementation risks introducing subtle bugs in stream handling, leading to brittle integrations and increased support burden. ## Solution: Built-in JSONL support in your SDKs Speakeasy addresses these challenges by automatically generating SDKs with sophisticated JSONL handling capabilities built directly into the client libraries you provide to your users. As an API provider, you simply define your streaming endpoints in your OpenAPI specification, and Speakeasy generates all the necessary data processing logic for your consumers. The resulting SDKs handle the entire workflow—from connection management to object deserialization—presenting your users with a clean, intuitive interface that feels like working with native language constructs rather than managing complex protocols. ## How to define JSONL in your OpenAPI Spec To use this feature, simply: 1. Define an operation with a response using `application/jsonl` content type 2. Specify the schema for individual objects in the stream Speakeasy then does the heavy lifting—generating the SDK logic that handles line-by-line parsing, validation, and type-safe data models automatically. For example: ```yaml /stream: get: responses: '200': content: application/jsonl: schema: # Your per-line object schema here type: object properties: id: type: integer message: type: string ``` ## Broader streaming support This new JSONL capability complements Speakeasy's existing robust support for handling Server-Sent Events (SSE) with the `text/event-stream` content type. While JSONL is a data format for structured information, SSE is an actual streaming transport mechanism built on HTTP. For more information about our SSE support: * [Defining SSE in OpenAPI](https://www.speakeasy.com/openapi/server-sent-events) * [Customizing SSE Runtime Behavior](https://www.speakeasy.com/docs/customize/runtime/server-sent-events) ## Availability & Automated handling SDK generation with built-in JSONL handling is available today for **Python** (using Pydantic models) and we are actively developing support for TypeScript, Go, Java, C#, PHP, Ruby and all future languages. For supported languages, generated SDKs automatically: * Read streams incrementally * Parse each JSON line individually * Validate each object against your OpenAPI-defined schema * Yield typed, ready-to-use data models ## Example: Effortless Python Experience ```python try: for record in client.operations.stream_log_data(): # 'record' is a typed Pydantic model matching your schema # Typed, validated, and ready-to-use! process_record(record.id, record.message) except ApiException as e: print(f"Error consuming stream: {e}") ``` Without Speakeasy's SDK generation capabilities, developers would need to implement multiple components themselves. Our approach encapsulates this complexity behind a clean, intuitive interface, significantly reducing development time and potential for errors. ### Benefits for your API consumers * **Rapid integration:** Eliminates boilerplate coding * **Increased reliability:** Reduces potential parsing and validation errors * **Superior developer experience:** Simplifies complex stream interactions **Get started today** 1. Define your streaming data formats natively in the spec with `text/event-stream` for SSE and `application/jsonl` for JSON Lines. 2. Generate your SDK: `speakeasy quickstart` 3. Empower your users with enhanced data handling capabilities. [See the documentation for a detailed guide.](/openapi/jsonl-responses) # release-model-context-protocol Source: https://speakeasy.com/blog/release-model-context-protocol [mcp]: https://modelcontextprotocol.io/introduction [mcp-announcement]: https://www.anthropic.com/news/model-context-protocol [docs]: /docs/standalone-mcp/build-server [universal-ts]: /post/introducing-universal-ts [standalone-funcs]: /post/standalone-functions [react-hooks]: /post/release-react-hooks
It's no longer enough for businesses to make their services available to developers. A great development experience also hinges on the ability for AI to access and integrate with available APIs. That's why starting today, every TypeScript SDK generated by Speakeasy now bundles a runnable [Model Context Protocol (MCP)][mcp] server enabling you to expose your API to the growing landscape of AI agents. ## What is MCP? [Model Context Protocol][mcp] (MCP) is an open source protocol developed by Anthropic for defining tools which connect AI agents to any 3rd party software system that has useful context. Since its [announcement][mcp-announcement] in November 2024, we've been seeing an increasing number of AI platforms adopting it. Through MCP servers, LLMs can visit websites, read files from your laptop, pull in messages from Slack and much more. For those familiar, the language server protocol that Microsoft released with Visual Studio Code had such a profound impact on how developers write code that it's been adopted in many other popular editors. Now, MCP is positioned to do the same for the agentic AI ecosystem. ## A type-safe MCP server in every SDK ```yaml filename="bluesky-ts/src/mcp-server" ├── tools │ ├── accountDelete.ts │ ├── accountExportData.ts │ ├── accountsGetInviteCodes.ts │ ├── actorGetSuggestions.ts │ └── ... ├── build.mts ├── mcp-server.ts ├── resources.ts ├── server.ts ├── shared.ts └── tools.ts ``` The generated MCP server acts as a thin wrapper around the existing TypeScript SDK, orchestrating API calls and formatting the results on behalf of the AI agent. The TypeScript SDK's generated Zod schemas are passed down to the MCP server to give the agent an accurate picture of the request format. For each method in the SDK, the MCP server will have a generated tool. The tool represents a discrete action that the AI agent can take. For example, the `bluesky-ts` SDK has a `getFeed` method that enables agents to fetch a user's feed from Bluesky. The MCP server will generate a `getFeed` tool that looks like this: ```typescript filename="bluesky-ts/src/mcp-server/tools/getFeed.ts" import { linksCreate } from "../../funcs/linksCreate.js"; import * as operations from "../../models/operations/index.js"; import { formatResult, ToolDefinition } from "../tools.js"; const args = { request: operations.CreateLinkRequestBody$inboundSchema.optional(), }; export const tool$linksCreate: ToolDefinition = { name: "links_create", description: `Create a new link Create a new link for the authenticated workspace.`, args, tool: async (client, args, ctx) => { const [result, apiCall] = await linksCreate(client, args.request, { fetchOptions: { signal: ctx.signal }, }).$inspect(); if (!result.ok) { return { content: [{ type: "text", text: result.error.message }], isError: true, }; } const value = result.value; return formatResult(value, apiCall); }, }; ``` ## Customizing tools with OpenAPI extensions We've added an `x-speakeasy-mcp` OpenAPI extension that will let you annotate your operations with custom tool names, descriptions and scopes. Being able to customize a tool's description in particular is going to be essential to provide an LLM with the right context on what a tool does and when to call it. ## Scopes Scopes are a small concept we're introducing that allows you to tag the generated tools and in turn allow users to control which of them are initialized when they start an MCP server. For example, it's possible to tag the "read" and "write" operations in your OpenAPI document like so: ```yaml paths: /todos: get: x-speakeasy-mcp: scopes: [read] operationId: listTodos # ... post: x-speakeasy-mcp: scopes: [write] operationId: createTodo # ... /todos/{id}: delete: x-speakeasy-mcp: scopes: [write, destructive] operationId: deleteTodo # ... ``` The server will now expose the `--scope` CLI flag: ```bash USAGE mcp start [--scope destructive|read|write] [--transport stdio|sse] [--port value] [--log-level debug|warning|info|error] [--server-url value] [--server-index value] [--api-host example.com|localhost:3000] mcp start --help Run the Model Context Protocol server FLAGS [--scope] Mount tools/resources that match given scope (repeatable flag) [destructive|read|write] [--transport] The transport to use for communicating with the server [stdio|sse, default = stdio] [--log-level] The log level to use for the server [debug|warning|info|error, default = info] [--port] The port to use when the SSE transport is enabled [default = 2718] [--server-url] Overrides the default server URL used by the SDK [--server-index] Selects a predefined server used by the SDK [--api-host] Sets the apiHost variable for url substitution [example.com|localhost:3000] -h [--help] Print help information and exit ``` And it can be launched like so: ```json { "mcpServers": { "Todos": { "command": "npx", "args": ["-y", "--", "todos", "mcp", "start", "--scope", "read"] "env": { "TODOS_API_TOKEN": "..." } } } } ``` > _Note the `--scope read` argument above_ Now only the tools that represent read operations will be running. This adds a layer of safety if you want to prevent an LLM from accidentally modifying or deleting data while exploring. ## Still an SDK at heart While the MCP server and CLI are going to be the common entrypoint for many users, we're still focused on shipping best-in-class TypeScript SDKs. Every tool we generate is a self-contained module that developers can import and compose into their own MCP servers or integrate into other frameworks. Here's a short example of using the `resolve-handle` tool from our Bluesky SDK and merge the result with data from your own project: ```ts import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; import { BlueskyCore } from "@speakeasy-sdks/bluesky/core.js"; import { tool$atprotoIdentityResolveHandle } from "@speakeasy-sdks/bluesky/mcp-server/tools/atprotoIdentityResolveHandle.js"; import * as z from "zod"; import { fetchProfileFields } from "./bsky.js"; const bluesky = new BlueskyCore(); const mcp = new McpServer({ name: "Mashup MCP", version: "0.0.0" }); const { tool: resolveDID, /* name, args (zod schema), description are also available here */ } = tool$atprotoIdentityResolveHandle; const schema = { handle: z.string(), fields: z.array(z.enum(["name", "bio", "avatar", "banner"])), }; mcp.tool( "bluesky-profile", "A tool that summarizes a Bluesky user's profile.", schema, async (args, extra) => { const res = await resolveDID( bluesky, { request: { handle: args.handle } }, extra, ); res.content.push({ type: "text", text: JSON.stringify(await fetchProfileFields(args.handle)), }); return res; }, ); ``` ## Get started with MCP If you are an existing Speakeasy customer generating TypeScript, you will already have a pull request ready to review that merge that adds MCP support to your SDK (and of course a way to opt out). For those that are new to Speakeasy, follow the SDK generation [getting started guide](/docs/sdks/create-client-sdks), and check out the [docs page][docs] that talks about enabling this feature and customizing your tools. ## What's next? The MCP ecosystem is nascent and we plan to do our part in growing it. The next big step is the planned support for remote servers. Despite not being released into the protocol yet, we're already seeing momentum from companies like [Cloudflare](https://developers.cloudflare.com/agents/concepts/tools/#model-context-protocol-mcp) getting ready to support it. There's no doubt that remote MCP servers will be a major improvement to the developer experience that will help to broaden their appeal to users. We're watching the project closely and will be adding support in the coming weeks. Stay tuned! # release-oss-arazzo-parser Source: https://speakeasy.com/blog/release-oss-arazzo-parser import { CodeWithTabs } from "@/mdx/components"; We've been hard at work on building an automated end-to-end testing framework for REST APIs. That's meant building a lot of internal tooling to work with the [Arazzo specification](https://github.com/OAI/Arazzo-Specification). Today, we're excited to announce the release of our Arazzo parser, a powerful tool for working with the Arazzo specification. We hope that this will be help the community build more tooling that leverages the Arazzo specification. [Check it out on GitHub →](https://github.com/speakeasy-api/openapi/tree/main/arazzo) ## What is Arazzo? The Arazzo specification is a language-agnostic way to describe chains of API calls. While OpenAPI documents define the individual API endpoints and methods available, Arazzo allows you to define sequences of calls that relate to those OpenAPI documents. This means you can create complex workflows like "authenticate → create user → get user → delete user" and ensure the output of one call feeds correctly into the inputs of another. What makes Arazzo particularly powerful is its ability to reference multiple OpenAPI documents from different providers, enabling you to build and test intricate integrations across various services. ## Why'd we build a parser? Developers testing API workflows often resort to writing custom testing applications or using end-to-end testing frameworks that don't integrate well with their API tooling. This leads to duplicate schema definitions and a disconnect between testing and the rest of the API development lifecycle. We see Arazzo bridging this gap by providing a native way to define workflows that leverage your existing OpenAPI specifications. This means you're not just testing individual API endpoints – you're validating entire workflows while ensuring your SDKs and integrations work as expected. ## Basic Example Here's a simple example of an Arazzo workflow for a hypothetical bar API: ```yaml workflows: createDrink: workflowId: create-drink summary: Creates a new drink in the system inputs: drinkName: string drinkType: string price: number steps: - operationId: authenticate inputs: username: ${inputs.username} password: ${inputs.password} - operationId: createDrink inputs: name: ${inputs.drinkName} type: ${inputs.drinkType} price: ${inputs.price} ``` ## What's in the parser The parser includes comprehensive features for: - **Reading** Arazzo documents - **Validating** documents - **Walking** through a document - **Creating** new documents - **Mutating** existing documents Here's how you can get started: ## What's Next? While the Arazzo specification is broad and deep, we're starting with focused support for testing workflows against single APIs. Our roadmap includes: - Visual workflow builders to make creating Arazzo documents more intuitive - Enhanced testing capabilities across multiple APIs - Integration with our existing SDK generation tools - Expanded tooling in our CLI for Arazzo validation and workflow management ## Try It Out The Arazzo parser is available now on GitHub. Get started by checking out our [documentation](https://www.speakeasy.com/openapi/arazzo) and let us know what you build with it! We're excited to see how the community uses Arazzo to improve their API testing and integration workflows. This is just the beginning of our journey to make API development more streamlined and reliable. # release-oss-openapi-library Source: https://speakeasy.com/blog/release-oss-openapi-library import { CodeWithTabs } from "@/mdx/components"; Today we're excited to announce the release of our OpenAPI parser library — a comprehensive Go library for working with OpenAPI 3.0.x and 3.1.x specifications, Arazzo workflow descriptions, and OpenAPI Overlay files that has been battle-tested on thousands of real-world API specs. [Check it out on GitHub →](https://github.com/speakeasy-api/openapi) ## Why we built this parser At Speakeasy, we process thousands of OpenAPI specifications from companies across all industries and sizes through our core SDK generation platform. More recently, through [Gram](https://app.getgram.ai) — our platform for building MCP (Model Context Protocol) servers — we've encountered even more diverse specifications in varying states of completeness and correctness. These specs come in all shapes and conditions — some perfectly valid, others with quirks, gaps, or outright errors that need to be handled gracefully. Gram, in particular, has been a catalyst for pushing our parsing capabilities further, as it needs to work reliably with imperfect specifications while automatically generating functional MCP servers. We needed a parser that could work with imperfect specs while providing the tools to improve them over time. This is especially important for API tooling that helps developers enhance their specifications incrementally, whether they're generating SDKs or building MCP servers. ## Core capabilities The library provides comprehensive functionality for working with OpenAPI documents: - **Parsing and validation**: Full support for OpenAPI 3.0.x and 3.1.x with built-in validation - **Document traversal**: Powerful walking API to iterate through specific elements - **Reference resolution**: Intelligent handling of `$ref` references, including circular references - **Document manipulation**: Create, modify, and transform OpenAPI documents programmatically - **Upgrading**: Automatically upgrade OpenAPI 3.0.x documents to 3.1.x - **Type safety**: Strongly typed Go structs that closely mirror the OpenAPI specification ## Getting started Here are some examples of what you can do with the parser: = 2 { return walk.ErrTerminate } return nil }, Schema: func(schema *oas3.JSONSchema[oas3.Referenceable]) error { if schema.IsLeft() && schema.GetLeft().Type != nil { types := schema.GetLeft().GetType() if len(types) > 0 { fmt.Printf("Found Schema of type: %s\\n", types[0]) } } return nil }, }) if err != nil { if err == walk.ErrTerminate { fmt.Println("Walk terminated early") break } fmt.Printf("Error during walk: %s\\n", err.Error()) break } } }`, }, { label: "mutate.go", language: "go", code: `package main import ( "bytes" "context" "fmt" "os" "github.com/speakeasy-api/openapi/openapi" "github.com/speakeasy-api/openapi/pointer" ) func main() { ctx := context.Background() r, err := os.Open("./openapi.yaml") if err != nil { panic(err) } defer r.Close() // Unmarshal the OpenAPI document doc, validationErrs, err := openapi.Unmarshal(ctx, r) if err != nil { panic(err) } // Print any validation errors for _, err := range validationErrs { fmt.Println(err.Error()) } // Mutate the document by modifying the returned OpenAPI object doc.Info.Title = "Updated Simple API" doc.Info.Description = pointer.From("This API has been updated with new description") // Add a new server doc.Servers = append(doc.Servers, &openapi.Server{ URL: "https://api.updated.com/v1", Description: pointer.From("Updated server"), }) buf := bytes.NewBuffer([]byte{}) // Marshal the updated document if err := openapi.Marshal(ctx, doc, buf); err != nil { panic(err) } fmt.Println("Updated document:") fmt.Println(buf.String()) }`, } ]} /> ## Advanced features The parser includes several advanced capabilities that set it apart: **Lazy reference resolution**: References are resolved on-demand, giving you fine-grained control over when and how references are loaded. This improves performance when working with large documents where you only need specific parts. **Circular reference handling**: Properly detects and handles circular references in schemas without infinite loops or stack overflow errors. **Extension support**: Full support for OpenAPI extensions (`x-*` fields) with type-safe access patterns. **Memory efficiency**: Optimized for working with large specifications while using minimal memory, important when processing hundreds of documents. ## Beyond OpenAPI: Arazzo and Overlays While OpenAPI documents define individual API endpoints, this library also supports two important extensions to the OpenAPI ecosystem: **Arazzo workflow specifications** allow you to describe chains of API calls and complex workflows. Instead of just defining what endpoints exist, Arazzo lets you describe sequences like "authenticate → create user → get user → delete user" where the output of one call feeds into the inputs of another. This is particularly powerful for testing API workflows and building integrations across multiple services. **OpenAPI Overlays** provide a way to modify existing OpenAPI documents without directly editing the original files. Overlays are separate documents containing instructions for updating, adding, or removing parts of an OpenAPI specification. This is invaluable for managing different versions of your API documentation, applying organization-specific customizations, or maintaining separate internal and external versions of your API specs. Both specifications integrate seamlessly with the core OpenAPI parsing functionality, giving you a complete toolkit for working with modern API specifications. ## What's next This release represents our foundational parsing capabilities across the OpenAPI ecosystem, but we have exciting plans ahead: - Enhanced tooling integration with our CLI for validation and transformation workflows - Additional utilities for common manipulation tasks across all supported specifications - Support for emerging OpenAPI specification features as they're released - Community-driven improvements and extensions ## Open source and community-driven This library is fully open source and we welcome contributions from the community. Whether it's bug reports, feature requests, or pull requests, we're excited to collaborate with developers who are building the future of API tooling. The codebase has been designed with contributors in mind — clean architecture, comprehensive tests, and clear documentation make it easy to understand and extend. ## Try it out The OpenAPI parser is available now on GitHub. Get started by checking out our documentation and examples, and let us know what you build with it! We're excited to see how the community uses this library to build better API tooling. This release represents years of learning from real-world API specifications, and we can't wait to see what innovative solutions the community creates with it. # release-php-ga Source: https://speakeasy.com/blog/release-php-ga Following our successful beta release and the continued renaissance in the PHP ecosystem, we're thrilled to announce that Speakeasy's PHP SDK Generation is now generally available. This marks a significant milestone in our mission to provide developers with powerful, type-safe SDKs across all major programming languages. ## Key highlights: - True type safety with PHP 8's robust type system - Native support for BigInt & Decimal with arbitrary precision - Built-in OAuth 2.0 flows with automatic token management - Seamless Laravel integration with ready-to-use Service Providers - Enhanced error handling with specialized exception types - Intelligent pagination for handling large datasets ## Why PHP matters in 2025 With PHP powering over 75% of websites with known server-side languages and the explosive growth of PHP 8's adoption, PHP remains a cornerstone of modern web development. The language has transformed from its dynamic roots into a powerhouse of type safety and performance, with PHP 8.0-8.3 introducing features that rival traditionally "enterprise" languages: ```php // The evolution of PHP type safety // PHP 5 - No type hints function process($data) { /* ... */ } // PHP 7 - Basic scalar type hints function process(array $data): array { /* ... */ } // PHP 8 - Full type system with union types and nullability function process(array $data, ?Logger $logger = null): array|JsonResponse { // Type-safe operations with IDE support return $data['success'] ? $data : new JsonResponse($data); } ``` Modern PHP offers the perfect balance between developer productivity and enterprise-grade reliability, making it a great choice for today's development teams. Our GA release brings production-ready PHP SDK generation to this thriving ecosystem, enabling API providers to deliver exceptional developer experiences to their PHP users. ## What we learned during the beta During our beta phase, we gathered valuable feedback from PHP developers across industries. Three main themes emerged: 1. Framework compatibility was crucial - especially with Laravel 2. Type safety was consistently identified as the most important feature 3. Performance optimization remained a key concern for high-volume API users We've addressed each of these areas in our GA release, with particular focus on framework integration and connection efficiency optimizations. --- ## What's new in our GA release Building on our beta foundation, we've expanded our PHP SDK generation with powerful new capabilities: ### Core features - **True Type Safety**: Leverage PHP 8's type system with proper docblocks for superior IDE integration - **Union Types**: Represent complex data structures and polymorphic APIs with PHP 8's union type syntax - **Laravel Integration**: Deploy framework-compatible packages with zero configuration required - **OAuth 2.0**: Implement secure authentication flows with built-in token management - **BigInt & Decimal**: Handle arbitrary precision numbers for financial and scientific applications ### Developer experience improvements - **Deep Object Support**: Pass complex nested objects in query parameters without serialization headaches - **Smart Pagination**: Navigate large datasets with intuitive iterators that handle pagination automatically - **Resilient Networking**: Implement robust retry mechanisms for handling transient API failures - **Extensible SDK Hooks**: Customize request/response workflows for logging, metrics, and more - **Comprehensive Docs**: Access usage examples for every endpoint with copy-pastable code samples ### Security Improvements All Speakeasy-generated PHP SDKs follow industry best practices for security, including: - **Ephemeral Token Storage**: Prevent persistence of sensitive credentials - **HTTPS-Only Connections**: Proper certificate validation for all requests - **Custom Security Policies**: Configurable security middleware for your needs - **Secure Error Handling**: Prevents sensitive information leakage ### Performance Improvements Performance is equally critical, especially for high-throughput applications: - **Minimal Dependencies**: We use only what's necessary to keep your dependency tree lean - **Connection Pooling**: Reuse HTTP connections for better performance under load - **Lazy Loading**: Resources are only initialized when accessed to reduce memory footprint - **PSR-18 HTTP Client**: Compatible with any PSR-18 client for maximum flexibility - **Request Batching**: Group multiple API calls into a single HTTP request where supported These optimizations ensure that your API integrations remain fast and reliable, even at scale. --- ## Core SDK Features In-Depth ### Type Safety PHP's journey from a loosely typed language to one embracing strong typing has been remarkable. Our SDK Generator maximizes PHP 8's type system to create truly type-safe SDKs that reduce runtime errors and improve developer productivity. Here's how our type-safe approach translates into real code: ```php class Payment { public string $id; public float $amount; public Currency $currency; public Status $status; public \DateTime $createdAt; public ?string $description; public Customer|Organization $owner; public function __construct( string $id, float $amount, Currency $currency, Status $status, \DateTime $createdAt, ?string $description, Customer|Organization $owner ) { $this->id = $id; $this->amount = $amount; $this->currency = $currency; $this->status = $status; $this->createdAt = $createdAt; $this->description = $description; $this->owner = $owner; } } ``` This approach ensures that your IDE can provide accurate auto-completion, type-related errors are caught early, and developers can build with confidence. We go even further by leveraging docstrings to provide additional type information, particularly for typed collections – something that PHP's built-in type system doesn't yet support natively: ```php /** * Process payments with enhanced type safety * * @param array $transactions List of transactions to process * @param ?PaymentProvider $provider Optional payment provider * @return array|ErrorResponse Collection of receipts or error response */ function processPayments( array $transactions, ?PaymentProvider $provider = null ): array|ErrorResponse { // Enhanced type-safe operations with IDE support for collection types // Your IDE now knows $transactions contains only Transaction objects // and the return value is either an array of Receipt objects or an ErrorResponse } ``` With these enhanced type annotations, IDEs can provide even more precise code completion and type checking than PHP's native type system alone allows. ### BigInt and Decimal Support Financial applications demand precision, and PHP's floating-point limitations often cause problems when handling currency values or large IDs. Our PHP SDKs solve this with native BigInt and Decimal support that maintains precision throughout the entire request/response lifecycle: ```php // Problem with regular floats/integers $regularFloat = 0.1 + 0.2; echo $regularFloat; // Outputs: 0.30000000000000004 (precision error) // With our Decimal support use YourAPI\Types\Decimal; $preciseDecimal = new Decimal('0.1')->add(new Decimal('0.2')); echo $preciseDecimal; // Outputs: 0.3 (exact precision) // Creating a transaction with precise decimal amounts $transaction = $client->transactions->create([ 'amount' => new Decimal('1234.56789012345'), 'accountId' => new BigInt('9007199254740991'), 'exchangeRate' => new Decimal('1.2345678901234567890') ]); // Performing precise calculations with returned values $fee = $transaction->amount->multiply(new Decimal('0.029')); $netAmount = $transaction->amount->subtract($fee); echo "Transaction Amount: {$transaction->amount}\n"; echo "Fee (2.9%): {$fee}\n"; echo "Net Amount: {$netAmount}\n"; ``` Our implementation preserves precision at every step in the data journey, from API request to business logic processing to storage — critical for financial services, blockchain applications, and scientific computing. ### OAuth 2.0 Implementing OAuth flows correctly can be challenging. Our PHP SDKs now include built-in support for OAuth 2.0, making secure API integration straightforward: ```php // Initialize client with OAuth credentials $client = Client::builder() ->setSecurity(new Security( $clientId = 'your-client-id', $clientSecret = 'your-client-secret' )) ->build(); // Generate authorization URL - redirect URL is configured at the provider level $authUrl = $client->auth()->getAuthorizationUrl([ 'scopes' => ['read', 'write'], 'state' => $csrf_token ]); // Redirect user to $authUrl // In your callback handler: $tokens = $client->auth()->exchangeCode($code); // The client will automatically use and refresh tokens for future requests $response = $client->resources->list(); ``` --- ## Laravel Integration PHP developers choose frameworks to increase productivity, with Laravel being the most popular choice. Our GA release includes first-class Laravel support with dependency injection, comprehensive testing utilities, and middleware compatibility. ### Simple Installation Via Composer ```bash composer require your-api/laravel-sdk ``` ### Automatic Service Provider Registration ```php // In config/app.php 'providers' => [ // Other Service Providers... YourAPI\Laravel\YourAPIServiceProvider::class, ], // In .env YOUR_API_KEY=sk_test_123456789 YOUR_API_ENVIRONMENT=sandbox ``` ### Dependency Injection In Controllers ```php use YourAPI\Client; use YourAPI\Resources\Payment; use YourAPI\Exceptions\ApiException; use Illuminate\Http\Request; class PaymentController extends Controller { public function __construct( private Client $client, // Auto-injected from the service container ) {} public function processPayment(Request $request) { try { $payment = $this->client->payments->create([ 'amount' => $request->input('amount'), 'currency' => Currency::USD, 'description' => $request->input('description'), 'metadata' => [ 'order_id' => $request->input('order_id'), 'customer_id' => auth()->id(), ], ]); // Store payment reference in your database $order = Order::find($request->input('order_id')); $order->payment_id = $payment->id; $order->payment_status = $payment->status; $order->save(); return view('payment.confirmation', ['payment' => $payment]); } catch (ApiException $e) { // Comprehensive error handling with specific error types return back()->withErrors([ 'payment' => $e->getMessage(), 'error_code' => $e->getErrorCode(), 'error_type' => $e->getErrorType(), ]); } } } ``` --- ## Real-World Integration Example To illustrate how these features work together, here's a simplified example of a subscription management system: ```php client = $client; // Register telemetry hooks for observability $this->client->registerHook(new YourAPI\Hooks\BeforeRequestHook()); $this->client->registerHook(new YourAPI\Hooks\AfterResponseHook()); } /** * Create a new subscription for a customer */ public function createSubscription(Customer $customer, string $planId): Subscription { try { // Create the subscription with typed parameters $paymentSubscription = $this->client->subscriptions->create([ 'customer' => $customer->payment_customer_id, 'plan' => $planId, 'billing_cycle_anchor' => new \DateTime('first day of next month'), 'proration_behavior' => 'none', ]); // Persist subscription details to local database $subscription = new Subscription([ 'customer_id' => $customer->id, 'payment_subscription_id' => $paymentSubscription->id, 'status' => $paymentSubscription->status, ]); $subscription->save(); return $subscription; } catch (ApiException $e) { // Type-safe error handling with specific exception types throw new PaymentFailedException($e->getUserFacingMessage()); } } /** * Process transactions for reconciliation with auto-pagination */ public function reconcileTransactions(\DateTime $startDate, \DateTime $endDate): array { $summary = [ 'total' => 0, 'successful' => 0, 'total_amount' => new Decimal('0'), ]; // SDK handles pagination automatically $transactions = $this->client->transactions->list([ 'created' => [ 'gte' => $startDate->format('Y-m-d'), 'lte' => $endDate->format('Y-m-d'), ], ]); // Auto-paginating iterator foreach ($transactions as $transaction) { $summary['total']++; if ($transaction->status === 'succeeded') { $summary['successful']++; $summary['total_amount'] = $summary['total_amount']->add($transaction->amount); } } return $summary; } } ``` This example demonstrates the key features of our SDK in a production context, showing how type safety, pagination, and decimal handling work together seamlessly. --- ## Get Started Today Ready to deliver a world-class developer experience to your PHP users? Here's how to get started: 1. [Generate your PHP SDK in less than 5 minutes](/docs/sdks/create-client-sdks) 2. [Read the documentation for configuration options](/docs/languages/php/methodology-php) PHP 8 has evolved into a powerful, type-safe language, and with our SDK generator, you can now provide your users with a developer experience that leverages all these capabilities. Try it out and see how it can simplify your API integration workflow. # release-php Source: https://speakeasy.com/blog/release-php ## PHP is so back Off the back of [Laravel's $57 million Series A](https://laravel-news.com/laravel-raises-57-million-series-a), we won't be the first to observe that ***PHP is so back***. Which makes it the perfect time to announce the beta release of our new PHP SDK Generator! We're bringing modern type safety and a streamlined developer experience to the PHP SDKs generated on our platform. ## Headline features Our PHP SDK Generator balances a simple developer experience with feature depth. Simple developer experience: - **Robust type safety** with carefully typed properties for all models - **Readability** for easy debugging with a streamlined, object-oriented approach - **Minimal external dependencies** for a lightweight footprint Depth: - **Union type support**, embracing PHP 8's modern type system - **Laravel service provider** for seamless integration - **SDK hooks** for customizing request workflows - **Pagination support** for handling large datasets efficiently - **Retry logic** for handling transient errors ## Type safety in PHP ### Humble beginnings Born as a loosely typed language, PHP spent years as the wild child of web development, allowing developers to play fast and loose with their data types. But as applications have grown more complex, the need for stricter type checking has become apparent. Enter PHP 7, which introduced scalar type declarations and return type declarations, marking the beginning of PHP's type matruity. Next, PHP 8 landed, bringing union types, null-safe operators, and other type-related improvements that firmly put PHP on the path to true type-safety. ### Building type safety into our SDK generation Our PHP SDK Generator takes fullest advantage of PHP's nascent type system. We've implemented a robust type-hinting system that leverages the latest PHP features to provide a truly type-safe developer experience. Here's how we've done it: 1. **Native Types**: Wherever possible, we use PHP's native types like `enum`, `string`, `int`, `float`, and `bool`. 2. **Class-Based Objects**: We generate standard PHP classes with public properties, using attributes and reflection to guide serialization. 3. **DateTime Handling**: We use the native `\DateTime` class for timestamp handling, ensuring consistent date and time operations. 4. **Custom Types**: For special cases like `Date`, we leverage the `Brick\DateTime\LocalDate` class from our minimal set of dependencies. Let's take a look at how this translates into real code: ```php class Drink { public string $name; public float $price; public ?DrinkType $type; public ?int $stock; public ?string $productCode; public function __construct(string $name, float $price, ?DrinkType $type, ?int $stock, ?string $productCode) { // Constructor implementation } } ``` This approach ensures that your IDE can provide accurate auto-complete suggestions, that type-related errors are caught early in the development process and that developers cannot accidentally build objects with incomplete data. ## Unions: Embracing PHP 8's Type System One of the most exciting features of PHP 8 was the introduction of union types, and our SDK Generator leverages this to provide even more precise type definitions. Union types allow properties or parameters to accept multiple types, providing flexibility while maintaining type safety. Here's how we've implemented union types in our generated SDKs: ```php class BeverageContainer { /** * * @var Shared\Mocktail|Shared\Cocktail $beverage */ public Shared\Mocktail|Shared\Cocktail $beverage; public function __construct(Shared\Mocktail|Shared\Cocktail $beverage) { $this->beverage = $beverage; } } ``` ## A Lightweight Footprint We understand the importance of keeping your project's dependency tree manageable. That's why our PHP SDK generator has been designed to rely on as few external libraries as possible. We've carefully selected a minimal set of dependencies: - `guzzlehttp/guzzle`: For a robust HTTP client - `jms/serializer`: To handle data serialization and deserialization - `Brick\DateTime`: For comprehensive date and time support This lean approach ensures that integrating our generated SDK into your project won't bloat your composer.json or introduce potential conflicts with your existing dependencies. ## Laravel-compatible packages for every API PHP generation comes with optional Laravel integration support that can be enabled in the SDK's `gen.yaml` configuration. This will generate a Laravel [Service Provider](https://laravel.com/docs/master/providers). The following snippet demonstrates an example of how to use the Laravel DI container with [Dub's PHP SDK](https://github.com/dubinc/dub-php). ```php use Dub\Dub; use Illuminate\Contracts\View\View; use Illuminate\Http\Request; final readonly class LinksController { public function __construct( private Dub $dub, ) {} public function index(Request $request): View { return view('links.index', [ 'links' => $this->dub->links->list(), ]); } } ``` ## Inject custom logic with hooks One of our most powerful new features is SDK hooks, which allow you to intercept and modify requests at various stages in their lifecycle. This gives you precise control over how your users' SDK interacts with your APIs, enabling customizations like: - Adding custom headers or query parameters - Modifying request bodies - Implementing complex authentication flows - Adding custom logging or monitoring Here's an example of how you might use hooks to make sure all requests are sent over `HTTPS`: ```php namespace Some\SDK\Hooks; use GuzzleHttp\ClientInterface; class HttpsCheckHook implements SDKInitHook { public function sdkInit(string $baseUrl, ClientInterface $client): SDKRequestContext { $parts = parse_url($baseUrl) if (array_key_exists('scheme', $parts) && $parts['scheme'] !== 'https') { $parts['scheme'] = 'https'; } $baseUrl = http_build_url($parts); return new SDKRequestContext($baseUrl, $client); } } ``` ## Enhanced Developer Experience We've gone the extra mile to ensure that working with our generated PHP SDKs is a joy for developers: - **Improved Import Patterns**: Say goodbye to namespace conflicts and hello to predictable behavior in your IDE's type hinting. - **Intuitive Factory Pattern**: Our convenient factory pattern manages the SDK configuration, making it a breeze to get started and customize as needed. - **Comprehensive Documentation**: Each generated SDK comes with detailed markdown documentation, ensuring you have all the information you need at your fingertips. ## Looking Forward As we enter this beta phase, we're excited to see how the PHP community puts our SDK Generator to work. We're committed to refining and improving the generator based on your feedback, so don't be shy – put it through its paces and let us know what you think! # release-postman-generator Source: https://speakeasy.com/blog/release-postman-generator import { Callout } from "@/mdx/components"; Postman is a popular tool for API development and testing, but it is tedius and often challenging to keep Postman Collections up-to-date with the latest API changes. To address this issue, we are excited to announce the release of our newest generation target: Postman Collections. This new generator means that you can now automatically generate high quality Postman Collections with the same OpenAPI documents, extensions, and workflows that you use today.
## Get Started If you are new to Speakeasy, you can learn more about how to [get started here](/docs/introduction/introduction). To get started just run: ```bash speakeasy configure targets ``` Once you have configured the Postman target, use `speakeasy run` to run the workflow. The Postman collection will be generated, ready for import into Postman We are very excited to share this with our community. Please give it a try and let us know what you think. We invite you to join our [Slack community](https://go.speakeasy.com/slack), and we look forward to your feedback and suggestions for future improvements. If you're new to Postman, read on to learn more about what Postman Collections offer and why they are important. ## What is a Postman Collection? A Postman Collection is a structured set of API endpoints and associated requests that can be imported into the Postman application for testing, development, and automation purposes. A collection encapsulates various operations like GET, POST, PUT, and DELETE requests, allowing developers to interact with APIs effectively and efficiently. ## Why are they useful to maintain? Maintaining Postman Collections facilitates the easy adoption, development, and testing of APIs. Collections allow developers to quickly execute API requests without setting up complex environments, speeding up both development and testing. Collections can also serve as a form of documentation, showing how APIs should be used, which is especially beneficial for onboarding new developers or for external users who need to understand the API's capabilities quickly. ## What makes maintenance hard? Despite their utility, maintaining Postman Collections is challenging. The difficulty comes from needing to keep collections up to date with the latest API changes. Manually updating collections in Postman is time-consuming and prone to errors, and the lack of versioning support in Postman Collections can lead to confusion and inconsistencies. These issues are all compounded when collections are forked by a large number of users. ## How does the generation work? Postman Collections can be automatically generated from an OpenAPI specification, which contains all the details of the API endpoints. Simply configure a Postman workflow target, run `speakeasy configure github`, and GitHub actions will automate generation. This automatic generation ensures that the collection always reflects any changes in the API specification, reducing the manual effort required and minimizing the risk of errors. ## How does this improve upon existing solutions? The automated generation of Postman Collections is a significant improvement over manual methods, and the Speakeasy OpenAPI feature set allows a much larger array of customization and functionality over existing automated solutions. This automation not only saves time but also enhances accuracy and consistency across different users and systems. By dynamically generating collections, developers can ensure that their APIs are always correctly represented and can immediately be tested for any updates or changes. # release-python-constructor-split Source: https://speakeasy.com/blog/release-python-constructor-split import { Callout } from "@/lib/mdx/components"; Python SDK generation now supports a constructor-based approach for handling synchronous and asynchronous operations. This eliminates method name duplication and provides a cleaner interface for SDK users. ## Same functionality, new interface Previously, Python SDKs used a method-based approach where every operation had two methods: a synchronous version and an asynchronous version with an `_async` suffix. This approach created several issues: - **Method name pollution**: Every operation appeared twice in autocomplete and documentation - **Naming confusion**: The `_async` suffix was awkward and non-standard - **Unclear intent**: The SDK interface didn't clearly communicate which mode it was in The new approach uses separate constructors for synchronous and asynchronous clients. All method names are now identical between sync and async versions. The choice happens once at instantiation, not repeatedly for every method call.
**Old approach: method-based** ```python sdk = MyAPI(api_key="...") # Synchronous operations result = sdk.list_users() # Asynchronous operations result = await sdk.list_users_async() ```
**New approach: constructor-based** ```python # Synchronous client sync_sdk = MyAPI(api_key="...") result = sync_sdk.list_users() # Asynchronous client async_sdk = AsyncMyAPI(api_key="...") result = await async_sdk.list_users() ```
The constructor pattern for handling async is more Pythonic with the added benefits of improved IDE suggestions (no duplicate method names) and enforcing clear user intent by making async usage a more explicit choice. ## Configuring the new behavior Constructor-based async can be enabled in your `gen.yaml` file: ```yaml python: version: 1.0.0 asyncMode: split # Use constructor-based approach ``` The `asyncMode` setting accepts two values: - `both` (default): Method-based approach with `_async` suffixes - `split`: Constructor-based approach with separate classes Switching to `asyncMode: split` is a breaking change. Existing SDK users will need to update their code to use the new constructor pattern. ## Why this matters Python's async ecosystem uses an event loop to manage non-blocking I/O operations, similar to JavaScript's event model. However, async functions in Python aren't easily interoperable with synchronous functions — async code "colors" all the functions it touches. While async is increasingly popular, synchronous code remains the default for many Python applications. Supporting both patterns cleanly requires a clear separation at the SDK level, which constructor-based async provides. This change aligns Python SDK generation with patterns used by established Python libraries and provides a more intuitive interface for developers familiar with Python's async conventions. ## Additional resources - [Python SDK generation design](/docs/languages/python/methodology-python) - [Gen.yaml configuration reference](/docs/speakeasy-reference/generation/gen-yaml) # release-python Source: https://speakeasy.com/blog/release-python import { CodeWithTabs } from "@/mdx/components"; import { Callout } from "@/mdx/components"; Python generation is GA (Generally Available)! Check out our [language maturity page](/docs/languages/maturity) for the latest information on language support. Today, we're announcing the release of our new Python Generator! The new generator takes full advantage of the best tooling available in the Python ecosystem to introduce true end-to-end type safety, support for asynchronous operations, and a streamlined developer experience. The full details are below, but here are the headline features that come included in the new Python SDKs: - Full type safety with [Pydantic](https://github.com/pydantic/pydantic) models for all request and response objects. - Support for both asynchronous and synchronous method calls using `HTTPX`. - Support for typed dicts as method inputs for an ergonomic interface. - `Poetry` for dependency management and packaging. - Improved IDE compatibility for a better type checking experience. - A DRYer and more maintainable internal library codebase. And if you want to see new SDKs in the wild, check out the SDK from our design partner: - [Dub.co](https://github.com/dubinc/dub-python) ## End-to-end Type Safety with Pydantic
Pydantic is a data modeling library beloved in the Python ecosystem. It enhances Python's type hinting annotations, allowing for more explicit API contracts and runtime validation. And now, Speakeasy generates Pydantic models for all request **and response** objects defined in your API. The request models ensure that your user's data is correct at run time, while the response models validate the data returned by the server matches the contract. The Pydantic-powered hints and error messages presented to users helps them ensure identify and correct errors before they cause issues downstream. This functionality is crucial for maintaining data integrity and reliability in applications that rely on your APIs. ## Enhanced Asynchronous and Synchronous Support
As the Python ecosystem has expanded to support data intensive, real-time applications, asynchronous support has grown in importance. That's why we've built our Python SDKs on top of `HTTPX`. Python SDKs will now support both asynchronous and synchronous method calls. And to make it as ergonomic as possible, there's no need for users to declare separate sync & async clients if they need both. Just instantiate one SDK instance and call methods sync or async as needed. ## Support for `TypedDict` Input ```python filename="main.py" from clerk_dev import Clerk s = Clerk() res = s.invitations.create(email_address="user@example.com", ignore_existing=True, notify=True, public_metadata={}, redirect_url="https://example.com/welcome") if res is not None: # handle response pass ``` Continuing with our efforts to make the SDKs as ergonomic as possible. SDKs now support the use of `TypedDict`s for passing data into methods. This feature allows you to construct request objects by simply listing off `key: value` pairs. The SDK will handle the construction of the request object behind the scenes. Just another way we're making it easier for your users to get integrated with your APIs. ## A Streamlined Developer Experience It's what on the inside that matters, so we've made significant improvements to the internal library code as well: - **Improved Import Patterns**: By refining how we handle imports internally, developers will see a more stable and predictable behavior in their IDE's type hinting. This change helps maintain a cleaner namespace and reduces the chance of conflicts or errors due to improper imports. - **Enhanced Dependency Management**: Transitioning from `pip` to `poetry` has streamlined our SDK's setup and dependency management, making it easier to maintain and update. `poetry` provides a more declarative way of managing project dependencies, which includes automatic resolution of conflicts and simpler packaging and distribution processes. - **Renamed Packages**: To further enhance usability, we've decoupled SDK class names from package names, allowing for more intuitive and flexible naming conventions. This adjustment allows better organization and integration within larger projects, where namespace management is crucial. - **DRYer Codebase**: We've refactored our internal library code to reduce redundancy and improve code reuse. This makes it easier for users to step through the codebase and understand how the SDK functions. These changes collectively reduce the complexity and increase the maintainability of projects using our SDK. ## Looking Forward The new Python Generator is just the beginning. We plan to continue refining the SDK based on user feedback. Over the next few weeks we'll be moving to make this new generation the default for all new Python SDKs generated on the platform. We are excited to see how the community puts these new features to work. Your feedback is invaluable to us, and we welcome everyone to join us in refining this tool to better suit the needs of the Python community. # release-react-hooks Source: https://speakeasy.com/blog/release-react-hooks import { CodeWithTabs } from "@/mdx/components"; import { Callout } from "@/mdx/components"; Now in preview! Enable React hook generation for your TypeScript SDKs through the Speakeasy CLI. Check out our [getting started guide](/docs/customize/typescript/react-hooks) to try it today. Today, we're excited to announce first-class support for React hooks in our TypeScript SDKs. This new generation feature builds on top of our [standalone functions](/post/standalone-functions) work and [TanStack Query](https://tanstack.com/query/latest) (formerly React Query) to provide seamless integration between your API and React applications. We think this feature is going supercharge teams to build awesome React applications against their APIs in the fewest steps possible. Used in roughly 1 in 6 React projects today, TanStack Query has become the de-facto standard for managing server state in React applications. Our new generator wraps your API operations, utilizing TanStack Query's powerful caching, synchronization, and data management features to create fully-typed hooks that are ready for use in your React app. Here are the key features you get out of the box: - Full type safety from your API through React components - Backed by runtime validation with [Zod](https://zod.dev) so you can trust your types - Great tree-shaking performance so you only bundle the hooks and SDK code you use - Automatic cache management with smart invalidation utilities - Support for both standard and infinite pagination patterns - Integration with server-side rendering, React Server Components and Suspense - Optimistic updates and background refetching - Smart request deduplication and request cancellation previousData, }, ); if (status === "loading") { return
Loading...
; } if (status === "error") { return
Error: {error.message}
; } const { followersCount = "-", followsCount = "-" } = data; return (
{data.displayName}

{data.displayName}

{data.handle}

{followersCount} followers

{followsCount} follows

); }`, }, { label: "actorProfile.ts", language: "typescript", code: `export function useActorProfile( request: operations.AppBskyActorGetProfileRequest, options?: QueryHookOptions, ): UseQueryResult { const client = useBlueskyContext(); return useQuery({ ...buildActorProfileQuery( client, request, options, ), ...options, }); }`, } ]} /> ## End-to-end Type Safety Our React hooks provide complete type safety from your API definition all the way through to your React components. Request and response types are derived from your OpenAPI specification and validated at runtime, ensuring your application stays in sync with your API contract. The type-safe interface helps developers catch errors early and enables rich IDE features like autocompletion and inline documentation. ## Intelligent Cache Management Managing cached data is one of the most challenging aspects of building modern web applications. Building on TanStack query, our React hooks handle this complexity for you by generating intelligent cache keys. We then provide utility functions to invalidate specific resources or groups of resources, making it easy to keep your UI in sync with your server state. The cache management system is built to handle common patterns like: - Optimistic updates for a snappy UI - Background refetching of stale data - Smart request deduplication - Automatic revalidation after mutations ## Support for SSR, RSC, Suspense and more Our React hooks are designed to work seamlessly with modern React patterns and features. We provide both standard and Suspense-enabled versions of each query hook, letting you choose the right approach for your application. Additionally, we provide utilities for prefetching data during server-side rendering and in React Server Components that will be immediately available to client components using the hooks. The hooks also support TanStack Query's latest features for handling loading states, preventing layout shift, and managing complex data dependencies. Here's an example of everything you get for each operation in your SDK: ```typescript import { // Query hooks for fetching data. useFollowers, useFollowersSuspense, // Query hooks suitable for building infinite scrolling or "load more" UIs. useFollowersInfinite, useFollowersInfiniteSuspense, // Utility for prefetching data during server-side rendering and in React // Server Components that will be immediately available to client components // using the hooks. prefetchFollowers, // Utilities to invalidate the query cache for this query in response to // mutations and other user actions. invalidateFollowers, invalidateAllFollowers, } from "@speakeasy-api/bluesky/react-query/followers.js"; ``` ## Pagination Made Simple We automatically detect pagination patterns in your API and generate appropriate hooks for both traditional pagination and infinite scroll interfaces. The infinite query hooks integrate perfectly with intersection observers for building smooth infinite scroll experiences. Here's an example using the [infinite query][infinite-query] version of a React hook: [infinite-query]: https://tanstack.com/query/v5/docs/framework/react/guides/infinite-queries ```typescript import { useInView } from "react-intersection-observer"; import { useActorAuthorFeedInfinite } from "@speakeasy-api/bluesky/react-query/actorAuthorFeed.js"; export function PostsView(props: { did: string }) { const { data, fetchNextPage, hasNextPage } = useActorAuthorFeedInfinite({ actor: props.did, }); const { ref } = useInView({ rootMargin: "50px", onChange(inView) { if (inView) { fetchNextPage(); } }, }); return (
    {data?.pages.flatMap((page) => { return page.result.feed.map((entry) => (
  • )); })}
{hasNextPage ?
: null}
); ``` ## Looking Forward We're excited to see how the community puts these new features to work. Your feedback is invaluable to us, and we welcome everyone to join us in refining these tools to better serve the React ecosystem. Try out the new React hooks today by updating your Speakeasy CLI and enabling React hooks generation for your TypeScript SDKs. Check out our documentation to get started. # release-sdk-docs Source: https://speakeasy.com/blog/release-sdk-docs {/* import { Testimonial } from "~/components"; */} {/* TODO: Add testimonials */} Production integration with an API involves a lot more than just making an HTTP request. So your docs need to do more than provide users with a generic `fetch` call. Today, we're partnering with [Mintlify](https://mintlify.com/) to release our new "code as docs" integration to help companies shift their API references to being code-native. You can now fully integrate your SDKs into your documentation provider so that building a production integration with your API is as easy as ⌘c, ⌘p.
## Building with Mintlify We spent the last two months in close collaboration developing the SDK Docs solution with the stellar team over at [Mintlify](https://mintlify.com/docs/integrations/sdks/speakeasy). Our shared commitment to Developer experience and building OpenAPI-based tooling made the team at Mintlify natural launch partners. We couldn't be more excited to work together towards our shared vision of making API integrations as easy as possible for developers. {/* */} ## How It Works For every method in your OpenAPI spec, we will generate code snippets that demonstrate use of your SDK to make the corresponding API request. We then add these snippets back into your spec using the `x-codeSamples` extension. To enable the new feature, you simply make a one line change to your Speakeasy workflow file: ```yaml filename=".speakeasy/workflow.yaml" targets: my-target: target: typescript source: my-source codeSamples: output: codeSamples.yaml ``` With your new workflow configured, we will regenerate snippets and create an updated OpenAPI spec. Point your docs provider at the new spec, and you're good to go! ## Supported documentation This feature was developed in partnership with Mintlify, but was designed for extensibility. It should be compatible with any documentation vendor that supports the `x-codeSamples` extension. That includes: - [Mintlify](https://mintlify.com/docs/integrations/sdks/speakeasy) - [Redoc](https://redocly.com/) - [Readme](https://readme.com/) - [Stoplight](https://stoplight.io/) - **Many more** ## The Future As we look ahead, the integration of SDKs into documentation platforms like Mintlify is only the beginning. We are working to enhance code snippets to be, not just copiable, but fully executable within a live sandbox environment. This transformative feature will empower developers to bootstrap production usage directly from the documentation pages. # release-sdk-release-notes Source: https://speakeasy.com/blog/release-sdk-release-notes SDK changes should be transparent and easy to validate. Today, we're announcing comprehensive SDK release notes that serve two critical purposes: giving SDK maintainers detailed summaries on every pull request to validate changes, and providing end users with clear documentation to track how the SDK is evolving over time. ## 📋 PR summaries & commit messages for SDK maintainers Every SDK generation results in a commit message that includes a comprehensive summary of the changes. Those commit messages, will be bundled into a PR summary that allows maintainers to easily validate SDK changes before clicking merge. The commit message will also include a line for each change made to the SDK, including: - **Added methods**: New functionality being introduced - **Removed methods**: Deprecated functionality being removed (flagged as breaking) - **Modified methods**: Changed signatures, parameters, or behavior - **Breaking change indicators**: Clear warnings for any backward-incompatible changes ![PR Summary Example](/assets/blog/release-sdk-release-notes/pr_summary.png) This improvement helps SDK maintainers with code review and maintain git history hygiene. ## 💬 Detailed release notes for SDK users Every SDK user deserves to know what's new and what's changed in the SDKs they rely on. This release includes detailed release notes that provide a comprehensive overview of the changes in each version. Once changes are validated and merged by maintainers, the detailed information becomes part of the SDK's public release notes: - **Method-level change tracking**: See exactly which methods were added, modified, or removed - **Breaking change visibility**: Clear indicators for any changes that could impact existing integrations - **Change summary**: Easy-to-scan summaries that makes it easy for users to assess upgrade impacts ## Getting started The feature works automatically with your existing Speakeasy workflow - no configuration changes required. Your next SDK update will include the enhanced release notes information in: - Pull request descriptions - Commit messages - Release notes # release-sdk-ruby-4 Source: https://speakeasy.com/blog/release-sdk-ruby-4 Language runtime upgrades happen regularly, and each one requires testing, compatibility fixes, and releases. With Speakeasy, this work happens automatically. Ruby 4.0 dropped on December 25, 2025, and as of today, we've updated our generators and perform compatibility tests so you don't have to. Your users get uninterrupted support for the latest Ruby version, and your team stays focused on your core product. ## What changed in Ruby 4.0 [Ruby 4.0](https://www.ruby-lang.org/en/news/2025/12/25/ruby-4-0-0-released/) was released on December 25, 2025, bringing exciting new features like Ruby Box for isolated definition spaces, the experimental ZJIT compiler, core language improvements including `Set` and `Pathname` promoted to core classes, as well as some general language clean up: - Removal of deprecated Ractor methods (`Ractor.yield`, `Ractor#take`) - Changes to `*nil` behavior (no longer invokes `nil.to_a`) - Removal of process creation via `IO`/`Kernel#open` with leading `|` - Various stdlib changes including CGI library removal from defaults None of these affected our generated SDKs significantly, but we verified compatibility so you don't have to wonder. ## Get started If you're already using Speakeasy to generate Ruby SDKs, you're all set. Regenerate your SDK using the latest Speakeasy version to pick up the improvements. If you're maintaining Ruby SDKs manually and want to stop worrying about runtime compatibility, [get started with Speakeasy](https://www.speakeasy.com/docs/create-client-sdks). # release-sdk-testing Source: https://speakeasy.com/blog/release-sdk-testing import { CodeWithTabs } from "@/mdx/components"; Today we're excited to announce the enhanced version of **SDK Testing**, a powerful addition to the Speakeasy Platform that transforms how you validate client libraries for your APIs. SDK Testing now provides comprehensive validation for SDKs in Go, TypeScript, and Python, with a focus on developer experience and efficiency. ### The Hidden Cost of SDK Testing For API providers, maintaining reliable client libraries comes with significant challenges: - **Manual SDK testing drains developer resources** with repetitive, error-prone tasks - **API evolution breaks test synchronization**, leading to outdated validation - **Breaking changes often reach customers** before they're caught in testing - **Complex API workflows** require sophisticated test sequences that are difficult to maintain Without robust testing, you risk developer frustration, reduced API adoption, and a surge in support issues. Our enhanced SDK Testing directly addresses these pain points. ### Comprehensive SDK Testing Solution Speakeasy SDK Testing approaches this problem holistically. We don't just generate the test code -- we create the entire testing environment including mock servers and realistic test data. ### Key Features - **Autogenerated tests from OpenAPI specs** with rich field coverage based on your schema definitions - **Zero-config mock servers** that simulate your API behavior without connecting to backend systems - **Example-aware test generation** that uses your OpenAPI examples or creates realistic test data - **GitHub Actions integration** for continuous validation on every PR - **Language-specific test implementations** that follow idiomatic patterns for each language - **Automatically updated tests** that evolve with your API changes - **Real server testing** for validating against actual API endpoints - **Multi-operation test workflows** using the [Arazzo specification](https://www.speakeasy.com/openapi/arazzo) for complex scenarios - **End-to-end validation** across entire user journeys ### Native Test Generation Tests are generated in your favorite language's native testing framework ([pytest](https://docs.pytest.org/en/stable/) for Python, [vitest](https://vitest.dev/) for TypeScript, etc.), ensuring they integrate seamlessly with your existing development workflow. We know that debugging impenetrable autogenerated tests is a nightmare, so we've put a lot of work into making our generated tests look and feel like they were written by your team. { const petstore = new Petstore({ serverURL: process.env["TEST_SERVER_URL"] ?? "http://localhost:18080", httpClient: createTestHTTPClient("createUser"), apiKey: process.env["PETSTORE_API_KEY"] ?? "", }); const result = await petstore.users.create({ id: 10, username: "theUser", firstName: "John", lastName: "James", email: "john@email.com", password: "12345", phone: "12345", userStatus: 1, }); expect(result).toBeDefined(); expect(result).toEqual({ id: 10, username: "theUser", firstName: "John", lastName: "James", email: "john@email.com", password: "12345", phone: "12345", userStatus: 1, }); });`, }, { label: "test_user_sdk.py", language: "python", code: `def test_user_sdk_create_user(): with Petstore( server_url=os.getenv("TEST_SERVER_URL", "http://localhost:18080"), client=create_test_http_client("createUser"), api_key="", ) as s: assert s is not None res = s.user.create_user( request={ "id": 10, "username": "theUser", "first_name": "John", "last_name": "James", "email": "john@email.com", "password": "12345", "phone": "12345", "user_status": 1, } ) assert res is not None assert res == petstore.User( id=10, username="theUser", first_name="John", last_name="James", email="john@email.com", password="12345", phone="12345", user_status=1, )`, }, { label: "user_test.go", language: "go", code: `func TestUser_CreateUser(t *testing.T) { s := petstoresdk.New( petstoresdk.WithServerURL(utils.GetEnv("TEST_SERVER_URL", "http://localhost:18080")), petstoresdk.WithClient(createTestHTTPClient("createUser")), petstoresdk.WithSecurity(""), ) ctx := context.Background() res, err := s.User.CreateUser(ctx, &components.User{ ID: petstoresdk.Int64(10), Username: petstoresdk.String("theUser"), FirstName: petstoresdk.String("John"), LastName: petstoresdk.String("James"), Email: petstoresdk.String("john@email.com"), Password: petstoresdk.String("12345"), Phone: petstoresdk.String("12345"), UserStatus: petstoresdk.Int(1), }) require.NoError(t, err) assert.Equal(t, 200, res.HTTPMeta.Response.StatusCode) assert.NotNil(t, res.User) assert.Equal(t, &components.User{ ID: petstoresdk.Int64(10), Username: petstoresdk.String("theUser"), FirstName: petstoresdk.String("John"), LastName: petstoresdk.String("James"), Email: petstoresdk.String("john@email.com"), Password: petstoresdk.String("12345"), Phone: petstoresdk.String("12345"), UserStatus: petstoresdk.Int(1), }, res.User) }`, } ]} /> ### Technical Implementation The SDK Testing framework operates through a streamlined architecture: 1. **Test bootstrapping**: The system analyzes your OpenAPI document and generates a `.speakeasy/tests.arazzo.yaml` file containing test workflows 2. **Mock server generation**: A language-specific mock server is created based on your API specification 3. **Test execution**: Tests run against the mock (or real) server, validating request/response handling 4. **Result aggregation**: Test outcomes are compiled into detailed reports Each test verifies critical aspects of SDK functionality: - Correct request formation (headers, path parameters, query strings, etc.) - Proper serialization and deserialization of complex objects - Accurate response handling and type conversions - Graceful error management ## End-to-End Testing with Arazzo Beyond simple contract testing, we're introducing end-to-end testing capabilities powered by the [Arazzo specification](https://www.speakeasy.com/openapi/arazzo). This allows you to validate complex workflows across multiple API endpoints with data dependencies between steps. ### What is Arazzo? Arazzo is a simple, human-readable specification for defining API workflows. It enables you to create rich tests that can: - Test sequences of multiple operations with dependencies - Extract data from responses to use in subsequent requests - Validate complex success criteria across operations - Test against both mock servers and real API endpoints - Configure setup and teardown routines for complex scenarios ### Example: User Lifecycle Testing Here's a simplified example of an Arazzo workflow that tests the complete lifecycle of a user resource: ```yaml arazzo: 1.0.0 workflows: - workflowId: user-lifecycle steps: - stepId: create operationId: createUser requestBody: contentType: application/json payload: { "email": "user@example.com", "first_name": "Test", "last_name": "User", } successCriteria: - condition: $statusCode == 200 outputs: id: $response.body#/id - stepId: get operationId: getUser parameters: - name: id in: path value: $steps.create.outputs.id successCriteria: - condition: $statusCode == 200 - stepId: update operationId: updateUser parameters: - name: id in: path value: $steps.create.outputs.id requestBody: contentType: application/json payload: $steps.get.outputs.user replacements: - target: /email value: "updated@example.com" successCriteria: - condition: $statusCode == 200 - stepId: delete operationId: deleteUser parameters: - name: id in: path value: $steps.create.outputs.id successCriteria: - condition: $statusCode == 200 ``` This workflow will automatically generate tests that: 1. Create a new user 2. Retrieve the user by ID 3. Update the user's information 4. Delete the user The tests share data between steps (like the user ID) and verify expected responses at each stage. ## ⚡ Getting Started in Three Simple Steps 1️⃣ **Generate tests** with `speakeasy configure tests`\ This will enable test generation in your configuration and create a `.speakeasy/tests.arazzo.yaml` file with default tests for all operations in your OpenAPI document.
2️⃣ **Run tests locally or in CI/CD** with `speakeasy test`\ This will run all tests against a mock server, validating your SDK's functionality without requiring a real backend.
3️⃣ **View reports** in the Speakeasy dashboard for insights. ![Test reports](/assets/blog/release-sdk-testing/changelog-2025-03-06-dashboard.png) ## Advanced Usage ### Customizing Tests You can customize tests by modifying the `.speakeasy/tests.arazzo.yaml` file. Some common customizations include: - **Grouping tests**: Use [`x-speakeasy-test-group`](/docs/sdk-testing/customizing-sdk-tests#grouping-tests) to organize tests into logical file groupings - **Target-specific tests**: Use [`x-speakeasy-test-targets`](/docs/sdk-testing/customizing-sdk-tests#generate-tests-only-for-specific-targets) to generate tests only for specific language targets - **Disabling tests for operations**: Add [`x-speakeasy-test: false`](/docs/sdk-testing/customizing-sdk-tests#disable-auto-generation-of-tests-for-specific-operations) in your OpenAPI spec for operations you don't want to test ### Testing Against Real APIs SDK Tests can be configured to run against your actual API endpoints instead of mock servers: - **Configuring server URLs**: Use [`x-speakeasy-test-server`](/docs/sdk-testing/api-contract-tests#configuring-an-api-to-test-against) to specify real API endpoints - **Security credentials**: Add [`x-speakeasy-test-security`](/docs/sdk-testing/api-contract-tests#configuring-security-credentials-for-contract-tests) to authenticate with your API - **Environment variables**: Use [`x-env`](/docs/sdk-testing/api-contract-tests#configuring-environment-variable-provided-values-for-contract-tests) syntax to inject environment variables into tests ### GitHub Actions Integration Run your tests automatically on every pull request with our out-of-the-box GitHub Actions workflow: ![A screenshot of a successful test check](/assets/blog/release-sdk-testing/testing-run.png) ![A screenshot of a successful test comment](/assets/blog/release-sdk-testing/testing-comment.png) ## Getting Started Ready to automate your API testing? SDK Testing is available today. You can generate tests in TypeScript, Python, and Go today, with more languages coming soon. [Sign up now](https://app.speakeasy.com/) or check out our [documentation](/docs/sdk-testing) to learn more. # release-speakeasy-docs Source: https://speakeasy.com/blog/release-speakeasy-docs Great documentation isn't just a nice-to-have — it's essential for developer adoption and success. We've heard from countless companies that while generating SDKs solved a crucial part of their API distribution challenge, they're still struggling to provide a documentation experience that matches their brand and meets their developers' needs. We're excited to announce the launch of Speakeasy Docs, powered by [Scalar](https://scalar.com/). This new offering combines Speakeasy's expertise in SDK generation with Scalar's best-in-class documentation platform to provide a complete solution so companies can deliver exceptional developer experiences. ## Too often docs let developers down Companies often face a difficult choice when building and maintaining API documentation: invest significant engineering resources into building the docs experience they want, or protect engineering resources by settling for a generic experience that doesn't meet their quality bar. Existing documentation solutions have problems such as customization limitations, OpenAPI spec rendering challenges, and difficulties maintaining consistency between their SDKs and documentation. These limitations put a ceiling on the developer experience they can deliver, impacting adoption and satisfaction. ## The docs your API deserves **Complete OpenAPI support**: Your API is more complicated than the pet store API. That's why Speakeasy Docs starts with industry-leading OpenAPI spec rendering. Your entire API, in all its messy glory, will be presented with accuracy and clarity in the generated documentation. **Customized branding**: Companies work hard to establish a unique identity, and your docs should reflect that. Customization is at the heart of Speakeasy Docs. You have complete control over the look and feel of your documentation. Your docs won't just contain your logo — they'll embody your brand's identity, matching your color schemes, typography, and custom design elements seamlessly. The result? Documentation that looks and feels custom-built for your brand. **Always in-sync**: We've also solved one of the most persistent challenges in API documentation: maintaining consistency between your SDKs and your docs. By integrating directly with our SDK generation platform, Speakeasy Docs ensures that your documentation and code samples are always in sync. When your API evolves, both your SDKs and documentation update automatically, eliminating the drift that often occurs between documentation and implementation. ## Why we partnered with Scalar Our partnership with Scalar is built on complementary strengths. While we've focused on building the best SDK generation platform, Scalar has established itself as a leader in API documentation, particularly when it comes to API reference rendering. This partnership allows both companies to focus on their core strengths: - Speakeasy: Advanced SDK generation and API tooling - Scalar: Design excellence and documentation expertise The result is a solution that's greater than the sum of its parts, offering our customers the best of both worlds without compromise. ### How the partnership works When you use Speakeasy Docs, you get the full Scalar solution without any limitations. You'll work with one vendor (us), with one support channel for both your docs & SDKs. Down the road, you'll see even deeper integration with new features that make the experience more seamless. ## Getting Started Getting started with Docs is straightforward and can be done entirely through our self-service dashboard. The platform works seamlessly with your existing OpenAPI specifications, and our team is here to help with any customization needs. For existing Speakeasy customers, just head to the Docs tab in the Speakeasy dashboard and follow the steps to enable it. For new customers, you can now get started with both SDK generation and documentation in one integrated solution by [signing up](https://app.speakeasy.com/). Join the growing number of companies that are choosing Speakeasy Docs to provide their API with the documentation experience it deserves! # release-speakeasy-suggest Source: https://speakeasy.com/blog/release-speakeasy-suggest import { Table } from "@/mdx/components"; [`Speakeasy Suggest`](/post/release-speakeasy-suggest), our AI-powered tool for automatically improving OpenAPI specs, is available now through the [Speakeasy CLI](/docs/speakeasy-cli/suggest/README) or as a [Github workflow](/docs/workflow-reference). Provide your own OpenAI API key, or simply use ours by default. We'd love to hear your feedback in our [Slack community](https://go.speakeasy.com/slack)! Stay tuned for more posts on `Suggest` output as Github PRs when configuring it as a workflow, interesting results on a variety of specs, and its downstream effects on Speakeasy-generated SDKs! ✨ We plan on open sourcing the core of our LLM-based agent for general-purpose use. Its primary function will be to serve as a JSON document transformer that receives a custom validation function, list of errors, and original document as inputs. ## Background At Speakeasy, we've been automating the creation of high-quality developer surfaces for API companies around the world. These include SDKs across multiple languages and even Terraform providers. Today, our generation logic is built on top of OpenAPI, requiring users to supply an OpenAPI schema to effectively use our tooling and generate these surfaces. Unfortunately, OpenAPI is a complicated specification with lots of potential for misconfiguration and unnecessary repetition. For example, the following schema operations contain duplicate operationIDs (i.e., `listDrinks`), duplicate inline schema objects, and no examples. ```yaml --- /drinks: get: operationId: listDrinks summary: Get a list of drinks. description: Get a list of drinks, if authenticated this will include stock levels and product codes otherwise it will only include public information. security: - {} tags: - drinks parameters: - name: drinkType in: query description: The type of drink to filter by. If not provided all drinks will be returned. required: false schema: $ref: "#/components/schemas/DrinkType" responses: "200": description: A list of drinks. content: application/json: schema: type: array items: type: object properties: name: description: The name of the drink. type: string type: $ref: "#/components/schemas/DrinkType" price: description: The price of one unit of the drink in US cents. type: number stock: description: The number of units of the drink in stock, only available when authenticated. type: integer readOnly: true productCode: description: The product code of the drink, only available when authenticated. type: string required: - name - price --- /drink/{name}: get: operationId: listDrinks summary: Get a drink. description: Get a drink by name, if authenticated this will include stock levels and product codes otherwise it will only include public information. tags: - drinks parameters: - name: name in: path required: true schema: type: string responses: "200": description: A drink. content: application/json: schema: type: object properties: name: description: The name of the drink. type: string type: $ref: "#/components/schemas/DrinkType" price: description: The price of one unit of the drink in US cents. type: number stock: description: The number of units of the drink in stock, only available when authenticated. type: integer readOnly: true productCode: description: The product code of the drink, only available when authenticated. type: string required: - name - price ``` As a result of this complexity, many companies create a spec to benefit from the vast ecosystem of OpenAPI tooling (e.g. docs providers), but they don't have resources or processes in place to maintain it as a first-class resource. At Speakeasy, we've worked with many users' specs to ensure they are compliant, clean, and accurately depict the state of their API. By leveraging our OpenAPI expertise and custom spec extensions, we've been able to create specs that produce idiomatic and human-readable SDKs for dozens of companies. **But wouldn't it be great if we could automate this? Enter Speakeasy Suggest.** ## Building Speakeasy Suggest ### What is it? `Speakeasy Suggest` is our LLM-based agent that, given an OpenAPI document, automatically suggests fixes, applies them, and outputs the modified spec. Using AI, we're able to offload the burden of spec management from API producers. `Suggest` can be invoked through both the Speakeasy CLI (outputs modified spec to the local filesystem) and our GitHub Action (creates PR) today. Applying `Suggest` on the invalid spec above, we produce a valid document with unique operationIDs, re-use of common objects where applicable, and better examples. ```yaml components: schemas: ... Drink: type: object properties: name: description: The name of the drink. type: string type: $ref: "#/components/schemas/DrinkType" price: description: The price of one unit of the drink in US cents. type: number stock: description: The number of units of the drink in stock, only available when authenticated. type: integer readOnly: true productCode: description: The product code of the drink, only available when authenticated. type: string required: - name - price example: name: Old Fashioned type: cocktail price: 1000 stock: 50 productCode: EX123 ... /drinks: get: operationId: listDrinks summary: Get a list of drinks. description: Get a list of drinks, if authenticated this will include stock levels and product codes otherwise it will only include public information. security: - {} tags: - drinks parameters: - name: drinkType in: query description: The type of drink to filter by. If not provided all drinks will be returned. required: false schema: $ref: "#/components/schemas/DrinkType" responses: "200": description: A list of drinks. content: application/json: schema: type: array items: $ref: "#/components/schemas/Drink" ... /drink/{name}: get: operationId: getDrink summary: Get a drink. description: Get a drink by name, if authenticated this will include stock levels and product codes otherwise it will only include public information. tags: - drinks parameters: - name: name in: path required: true schema: type: string example: CocaCola responses: "200": description: A drink. content: application/json: schema: $ref: "#/components/schemas/Drink" ... ... ``` For the remainder of this post, we'd like to delve deeper into the decision-making and engineering challenges behind `Speakeasy Suggest`. To do that, we'll start from the beginning. ### Good Input = Good Output Just as a polished OpenAPI spec may be used to generate high-quality SDKs, an LLM requires input with sufficient specificity and detail to produce useful suggestions for a document. It became quickly obvious to us that dumping a spec and naively asking an LLM to “make it better” would not be fruitful. We found that our in-house validator (i.e. `speakeasy validate` from our [CLI](https://github.com/speakeasy-api/speakeasy)) would be effective at adding a more specific, deterministic context to our LLM prompt. Our validator is built using [vacuum](https://github.com/daveshanley/vacuum), enabling us to specify **custom rules**. Since these rules are defined by us, they output fixed error messages and line numbers for an array of spec issues. Below is a table highlighting a few of the errors, the suggestions we'd hope to receive from the LLM when including them in the prompt, and explanations of the resulting effects on Speakeasy-generated SDKs when applying those suggestions to the OpenAPI spec.
Each of the validation errors above is present in our invalid spec. So let's examine the generated Go SDK code before and after running `Speakeasy Suggest` on the document. **Before** ```go // ListDrinks - Get a drink. // Get a drink by name, if authenticated this will include stock levels and product codes otherwise it will only include public information. func (s *drinks) ListDrinks(ctx context.Context, request operations.ListDrinksRequest) (*operations.ListDrinksResponse, error) { ... switch { case httpRes.StatusCode == 200: switch { case utils.MatchContentType(contentType, `application/json`): var out *operations.ListDrinks200ApplicationJSON if err := utils.UnmarshalJsonFromResponseBody(bytes.NewBuffer(rawBody), &out); err != nil { return nil, err } res.ListDrinks200ApplicationJSONObject = out ... } ... } return res, nil } // ListDrinks - Get a list of drinks. // Get a list of drinks, if authenticated this will include stock levels and product codes otherwise it will only include public information. func (s *drinks) ListDrinks(ctx context.Context, request operations.ListDrinksRequest) (*operations.ListDrinksResponse, error) { ... switch { case httpRes.StatusCode == 200: switch { case utils.MatchContentType(contentType, `application/json`): var out []operations.ListDrinks200ApplicationJSON if err := utils.UnmarshalJsonFromResponseBody(bytes.NewBuffer(rawBody), &out); err != nil { return nil, err } res.ListDrinks200ApplicationJSONObjects = out ... } ... } return res, nil } ``` #### After ```go // GetDrink - Get a drink. // Get a drink by name, if authenticated this will include stock levels and product codes otherwise it will only include public information. func (s *drinks) GetDrink(ctx context.Context, request operations.GetDrinkRequest) (*operations.GetDrinkResponse, error) { ... switch { case httpRes.StatusCode == 200: switch { case utils.MatchContentType(contentType, `application/json`): var out *shared.Drink if err := utils.UnmarshalJsonFromResponseBody(bytes.NewBuffer(rawBody), &out); err != nil { return nil, err } res.Drink = out ... } ... } return res, nil } // ListDrinks - Get a list of drinks. // Get a list of drinks, if authenticated this will include stock levels and product codes otherwise it will only include public information. func (s *drinks) ListDrinks(ctx context.Context, request operations.ListDrinksRequest) (*operations.ListDrinksResponse, error) { ... switch { case httpRes.StatusCode == 200: switch { case utils.MatchContentType(contentType, `application/json`): var out []shared.Drink if err := utils.UnmarshalJsonFromResponseBody(bytes.NewBuffer(rawBody), &out); err != nil { return nil, err } res.Drinks = out ... } ... } return res, nil } ``` The code in “Before” would not even compile due to duplicate method names from the conflicting operationIDs. As such, we wouldn't even generate the SDK without throwing a validation error but have done so here simply to show the differences in output. Also, notice the stark difference in the generated class names the successful responses are being deserialized to (i.e. `ListDrinks200ApplicationJSON` vs. `Drink`). Let's also examine the differences in the README's usage snippets your users will reference in their implementation. **Before** ```go ... func main() { s := sdk.New( sdk.WithSecurity(shared.Security{ APIKey: "", }), ) ctx := context.Background() res, err := s.Drinks.ListDrink(ctx, operations.ListDrinkRequest{ Name: "Willie Gulgowski DVM", }) if err != nil { log.Fatal(err) } if res.ListDrink200ApplicationJSONObject != nil { // handle response } } ... ``` **After** ```go ... func main() { s := sdk.New( sdk.WithSecurity(shared.Security{ APIKey: "", }), ) ctx := context.Background() res, err := s.Drinks.GetDrink(ctx, operations.GetDrinkRequest{ Name: "CocaCola", }) if err != nil { log.Fatal(err) } if res.Drink != nil { // handle response } } ... ``` In addition to better method names and more human-readable classnames, the example value for the drink name parameter is much more relevant. This makes the generated SDK easier for end-users to consume and integrate into their applications. The validation errors we've applied suggestions for are just a small sample of the improvements we can make to an OpenAPI spec. By including them in the prompt, we've enabled a feedback loop where even non-critical errors that don't block SDK generation are encouraged to be added if addressing them improves the quality of the SDK output. Let's delve into how we receive these suggestions from the LLM in a format that allows us to apply them to the spec. ## Structured Output Parser In our initial implementation of `Suggest`, we were only focused on reporting suggestions instead of applying them. Unfortunately, this requires a human in the loop to copy-paste the suggestions themselves. To avoid potential errors and automate the spec management, we needed to be involved in the process of applying the suggestions to our input document as well. This would enable us to revalidate the document upon each applied fix and ensure that overall document correctness was being improved. Modifying an OpenAPI schema requires us to receive structured output from the LLM that we can apply to the document. We decided to use JSON patch for the format of the suggestion itself. Using [langchain-js](https://github.com/langchain-ai/langchainjs), this involved defining a custom agent with an output parser specifying the [zod](https://github.com/colinhacks/zod) schema we pass in to ensure we get LLM output in the right format. ```tsx // zod schema export const SUGGESTION_SCHEMA = z.object({ suggested_fix: z.string().describe( `a simple English description of the suggested JSON patch. For example, the JSON patch [{"op": "replace", "path": "/paths/~1pet/post/operationId", "value": "addPet"}] could be described as "Replace the operationId of the POST /pet endpoint with addPet` ), json_patch: z.string().describe(`suggested fix in the form of a JSON Patch`), reasoning: z .string() .describe( "a brief explanation of why you think the suggested fix will resolve the error" ), }); ... // output parser which verifies LLM's final response is formatted according to the zod schema class JsonOutputParser extends AgentActionOutputParser { lc_namespace = ["langchain", "agents", "custom_llm_agent"]; schema: z.ZodType; constructor(schema: z.ZodType) { super(); this.schema = schema; } async parse(text: string): Promise { console.log("\n" + text); const answerMatch = /```json(.*)```/gms.exec(text); if (answerMatch) { try { const json = JSON.parse(answerMatch[1].trim()); // This will throw an error if the json does not match the schema. this.schema.parse(json); return { log: "Got answer", returnValues: json, }; } catch (e) { console.log( "Answer is invalid JSON or does not match schema. Trying again..." ); return { tool: "logger", toolInput: `Error: ${e}`, log: `Final answer is invalid. It does not conform to the requested output schema. Output schema: ${JSON.stringify( zodToJsonSchema(this.schema) )}`, }; } } ... } ... } ``` This ensured responses were in a form that was easy for us to extract the JSON patch from. However, we soon found that our agent would hallucinate invalid JSON patches that referenced keys that didn't exist in the OpenAPI document. This required augmenting our agent with another tool to verify the validity of these patches by checking if we could apply them. ```tsx export class JsonPatchValidator extends DynamicTool { static description = `Use this to verify that a given JSON patch is valid. Always use this tool to check the validity of your final suggestion. If your suggestion is invalid, use the error message to try to fix it. Do not return anything this tool says is invalid as your answer. This tool will return "valid" if the patch is valid, or an error message if it is not. Example input to this tool: [{{ "op": "replace", "path": "/baz", "value": "boo" }}] Example output: "valid", "error: path /baz does not exist"`; constructor(spec: JSON) { super({ name: "json_patch_valid", func: async (input) => { input = removeQuotes(input); try { apply_patch(spec, input); return "valid"; } catch (e: any) { return `error: ${e}`; } }, description: JsonPatchValidator.description, }); } } ``` 💡For a yaml spec, `Speakeasy Suggest` takes extra caution by converting it to JSON before applying the patch and converting it back to yaml while preserving the original key ordering. We'll discuss several of the tools we equipped our agent within the next section, where we detail the decision-making process of moving forward with a custom agent. ## Benchmarking Different Approaches There were two main approaches we tested — _Custom Agent vs. Retrieval Augmented Generation (RAG)_. In order to not exceed GPT-4's token limits, we provide a small range of lines surrounding the error from the spec to the LLM prompt. As a result, both approaches need to be capable of enabling the LLM to gather information from the full spec in order to return valid JSON patches. The primary differentiator is how this is achieved: - An agent utilizes tools to enable the LLM to execute custom logic to traverse the spec - In RAG, we use external data to augment our prompts. In our case, this involves the standard method of using a vector store to persist embeddings of the spec, which the LLM can quickly index when trying to answer the prompt. ### **Agent with Custom Tools** Directly from the langchain docs: “In agents, a language model is used as a reasoning engine to determine which actions to take and in which order.” Each action an agent can take corresponds to one of the tools defined by us that executes a custom function. Since we're using GPT-4 as our LLM, this takes advantage of [OpenAI's function calling](https://openai.com/blog/function-calling-and-other-api-updates). Our tools take the OpenAPI spec as input and are capable of performing a range of operations on it. **Searching for line numbers that contain a particular string** The LLM may need to locate regions of the spec that are not in close proximity to the validation error but contain a specific key. ```bash WARN validation warn: [line 63] oas3-unused-component - `#/components/schemas/Pets` is potentially unused or has been orphaned Using model gpt-4-0613 Running agent... The error message indicates that the schema 'Pets' is potentially unused or has been orphaned. This means that it is not referenced anywhere in the document. To confirm this, I will use the document_text_searcher tool to search for any references to 'Pets' in the document. If there are no references, the schema 'Pets' can be removed from the document. If there are references, they need to be replaced with the correct schema. Action: document_text_searcher Action Input: 'Pets' ``` From the LLM logs, we can see that the `document_text_searcher` tool searches the document for `Pets` to see if any usage of this component exists across the schema. The wonderful thing about agents is that the result of this action can be fed as input to other tools before returning a final answer (see `get_line_range` below). **Retrieving specific line ranges** This is helpful since our validation errors - and the output of other tools - report the offending line number. The LLM uses this to retrieve the region of the spec it needs more information from or where most fixes are likely to be applied. Following the output of `document_text_searcher` above, the LLM receives appropriate line numbers to conclude its search and validate the final answer. ```bash The 'Pets' schema is referenced on lines 5 and 99. Line 99 is the definition of the 'Pets' schema itself, so it doesn't count as a usage. I need to check line 5 to see how 'Pets' is used. I will use the get_line_range tool to retrieve lines 1-20 for context. Action: get_line_range Action Input: '1-20' The 'Pets' schema is not used in lines 1-20. It seems that the 'Pets' schema is indeed unused. I will suggest a JSON patch to remove the 'Pets' schema from the document. I will also validate the JSON patch using the json_patch_valid tool. Action: json_patch_valid Action Input: [{"op": "remove", "path": "/components/schemas/Pets"}] The JSON patch is valid. I will now provide the final answer. Final Answer: { "suggested_fix": "Remove the unused 'Pets' schema from the components.", "json_patch": "[{\"op\": \"remove\", \"path\": \"/components/schemas/Pets\"}]", "reasoning": "The 'Pets' schema is not referenced anywhere in the document, making it unused and can be safely removed." } ``` **Resolving $ref definitions** Many OpenAPI specs contain `$ref` keys that point to definitions living in different parts of the document. The LLM should be able to locate the original definition from this when it needs to use it to suggest a fix. This is useful when we want to suggest an example for a component in the OpenAPI spec. ```bash INFO validation hint: [line 104] missing-examples - Missing example for component. Consider adding an example Using model gpt-4-0613 Running agent... The error message indicates that there is a missing example for a component. Looking at the surrounding context, it seems that the error is on line 162, which corresponds to the "Pets" schema. This schema is an array of "Pet" objects. To fix this error, we need to add an example for the "Pets" schema. The example should be an array of "Pet" objects. We can use the "Pet" schema to create a valid example. Action: ref_resolver Action Input: #/components/schemas/Pet The "Pet" schema requires an "id" and a "name", both of which are strings. An example "Pet" could be {"id": 1, "name": "Fido", "tag": "dog"}. We can use this to create an example for the "Pets" schema, which is an array of "Pet" objects. The JSON patch to add this example would be [{"op": "add", "path": "/components/schemas/Pets/example", "value": [{"id": 1, "name": "Fido", "tag": "dog"}]}]. Let's validate this patch. Action: json_patch_valid Action Input: [{"op": "add", "path": "/components/schemas/Pets/example", "value": [{"id": 1, "name": "Fido", "tag": "dog"}]}] The JSON patch is valid. Therefore, we can suggest this patch as a fix for the error. The patch adds an example to the "Pets" schema, which is an array of "Pet" objects. The example is a valid "Pet" object, as it includes the required "id" and "name" properties. Final Answer: { "suggested_fix": "Add an example to the 'Pets' schema. The example should be an array of 'Pet' objects. An example 'Pet' object could be {'id': 1, 'name': 'Fido', 'tag': 'dog'}.", "json_patch": "[{\"op\": \"add\", \"path\": \"/components/schemas/Pets/example\", \"value\": [{\"id\": 1, \"name\": \"Fido\", \"tag\": \"dog\"}]}]", "reasoning": "The error message indicates that there is a missing example for a component. The 'Pets' schema is an array of 'Pet' objects, so the example should also be an array of 'Pet' objects. The 'Pet' schema requires an 'id' and a 'name', so the example 'Pet' object includes these properties." } ``` By equipping the agent with the necessary toolkit to explore the spec JSON, we are able to traverse the document without exceeding the TPM (tokens per minute) limit. However, since each initial query to the agent can chain through a sequence of actions before returning a final answer, we wanted to assess another approach that would limit the number of OpenAI calls. **Retrieval Augmented Generation (OpenAPI Spec Embeddings in Chroma)** We use embeddings to store the spec for quick similarity searches without requiring the full contents to be passed into the prompt, enabling us to respect the token limit. Specifically, we used [RetrievalQAChain](https://js.langchain.com/v0.1/docs/modules/chains/popular/vector_db_qa) with the OpenAPI spec embedded in [Chroma](https://github.com/chroma-core/chroma). **Results** We compared the execution time, number of OpenAI calls, and the accuracy of both approaches above. Although RAG seemed to perform fine on **\***most**\*** simple errors, complex tasks such as moving duplicate inline schemas to shared components seemed impossible. At first, the returned JSON patch for such a task seems reasonable. ```bash INFO {"error":"validation hint: [line 22] duplicate-schemas - Identical inline object schemas found: [line 22] - [line 31] conflicts with [line 107] - [line 116]. Consider moving to a shared component"} Asking for a Suggestion! [{"op": "add", "path": "/components/schemas/Error", "value": {"type": "object", "required": ["code", "message"], "properties": {"code": {"type": "integer", "format": "int32"}, "message": {"type": "string"}}}}, {"op": "replace", "path": "/paths/~1pets/get/parameters/0/schema", "value": {"$ref": "#/components/schemas/Error"}}, {"op": "replace", "path": "/components/schemas/Error", "value": {"$ref": "#/components/schemas/Error"}}] ``` But upon further review, we found it added the shared component and then swapped out its own schema definition with the `$ref`, thinking it was one of the duplicate schemas itself despite having a clear line number to check against. Since RAG can't gather more context via function calls, it would use a similarity search to suggest fixes for same-name fields on the wrong lines. Due to this behavior, this approach wasn't sufficient for returning JSON patches with the correct paths. The agent fared much better on all validation errors ranging across a broad span of complexity. Despite the fact that it took 2-3x as long to execute, the agent's ability to “reason” through a sequence of actions via the execution of custom functions proved to be critical for good suggestion quality. The ability to equip the agent with any new tool we could think of makes it more flexible and future-proof for our use-case than embedding the OpenAPI schema. This is the most important factor in ensuring `Suggest` is a useful product, as OpenAPI specs vary greatly in shape, purpose, and functionality, much like the tools and people who produce them. ### Conclusion Even after settling on the agent approach, suggestion quality, rate limits, and execution time were all variables that still needed to be addressed. Our team has been tackling these roadblocks by implementing parallelization, passing in additional guidance to the prompt for specific validation errors, and benchmarking various LLMs post fine-tuning. Want to try it for yourself? Head over to the [Speakeasy Suggest docs](/post/release-speakeasy-suggest), or just copy paste [this workflow file](https://github.com/speakeasy-sdks/template-sdk/blob/main/.github/workflows/speakeasy_spec_suggestions.yml) into your own github repo to get started! # release-sse-improvements Source: https://speakeasy.com/blog/release-sse-improvements SDK generation now includes major improvements to Server-Sent Events (SSE) support, making it easier to work with streaming APIs across TypeScript, Python, Java, and C#. ## Why SSE matters Server-Sent Events enable servers to push real-time updates to clients over a single HTTP connection. This pattern is essential for streaming APIs, particularly AI and LLM services like OpenAI, Anthropic, and similar platforms where responses stream token-by-token. These improvements eliminate boilerplate code and provide better type safety when working with streaming responses. ## 🎯 Type-safe SSE overloads TypeScript and Python SDKs now generate type-safe overloads for operations that support both streaming and non-streaming responses. Previously, operations with a `stream: true/false` parameter returned a union type. The type system couldn't determine the return type even though the `stream` parameter value provided the relevant context, leaving developers to write their own handlers. We've revamped generated SDKs to now include overloads that return the correct type based on the `stream` parameter. IntelliSense and type checking now work correctly without manual type guards.
**Old behavior: union return type** ```typescript const response = await sdk.chat.completions({ stream: true, messages: [...] }); // Type system doesn't know which type was returned if (response instanceof EventStream) { // Handle streaming response } else { // Handle JSON response } ```
**New behavior: overloaded methods** ```typescript const streamResponse = await sdk.chat.completions({ stream: true, // Returns EventStream messages: [...] }); // TypeScript knows this is EventStream const jsonResponse = await sdk.chat.completions({ stream: false, // Returns CompletionResponse messages: [...] }); // TypeScript knows this is CompletionResponse ```
There is no configuration required to enable SSE overloads. They are automatically generated when a streaming operation follows the established pattern, i.e has a required request body containing a boolean `stream` field and returns different response types based on the `stream` value. ## 📦 SSE response flattening TypeScript and Python SDKs support flattening SSE responses to eliminate unnecessary nesting. SSE responses include metadata fields (`event`, `data`, `retry`, `id`). When the `event` field acts as a discriminator, it forces nested access patterns. Developers must always access `.data` to reach the actual response content, even when the outer `event` field already determined the type. Response flattening abstracts away the `event`, `retry` and `id` fields from the server sent event and just yields the `data` in an un-nested fashion.
**Without flattening: nested access required** ```typescript for await (const response of stream) { if (response.event === "message") { console.log(response.data.content); } else if (response.event === "error") { console.error(response.data.message); } } ```
**With flattening: direct access** ```typescript for await (const response of stream) { if (response.type === "message") { console.log(response.content); // No .data nesting } else if (response.type === "error") { console.error(response.message); // No .data nesting } } ```
To configure flattening in your `gen.yaml`: ```yaml typescript: version: 1.0.0 sseFlatResponse: true python: version: 1.0.0 sseFlatResponse: true ``` ## 🔷 C# SSE support C# SDKs now include full Server-Sent Events support, bringing streaming capabilities to .NET applications. ```csharp // C# streaming example var request = new CompletionRequest() { Stream = true, Messages = messages }; var res = await sdk.Chat.Completions(request) using (var eventStream = res.Events!) { CompletionEvent? eventData; while ((eventData = await eventStream.Next()) != null) { Console.Write(eventData); } } ``` C# joins TypeScript, Python, Go, Java, PHP, and Ruby with complete SSE support. ## ☕ Java for-each support for SSE Java SDKs now support for-each iteration over SSE streams, providing a more idiomatic way to process streaming responses. ```java // Java streaming with for-each CompletionRequest request = CompletionRequest.builder() .stream(true) .messages(messages) .build(); CompletionResponse res = sdk.chat().completions(request); try (EventStream events = res.events()) { for (ChatStream event : events) { System.out.println(event); } } ``` This eliminates the need for manual iterator handling and makes streaming code cleaner. ## Additional resources - [TypeScript SDK generation](/docs/languages/typescript/methodology-ts) - [Python SDK generation](/docs/languages/python/methodology-python) - [Java SDK generation](/docs/languages/java/methodology-java) - [C# SDK generation](/docs/languages/csharp/methodology-csharp) # release-standalone-mcp Source: https://speakeasy.com/blog/release-standalone-mcp [Gram by Speakeasy](https://app.getgram.ai/login) is the fastest way to create, curate and host MCP servers in production. It's an all in one platform for your team. But for organizations that prefer a bare-metal approach, we're excited to introduce the new self-hostable MCP target. This dedicated target creates an MCP server code repository, which can be deployed as a remote server or used locally. It's a perfect fit for those who want total control over the internals of their MCP servers. ## A fully featured MCP server The new self-hostable MCP target is an evolution of the [SDK-bundled version](/blog/release-model-context-protocol). By creating a dedicated target, more MCP-specific features have been added, for a more fully featured server. ### Automatic DXT generation Think of DXT files as the "app store for AI tools" — but instead of downloading apps, you're installing powerful MCP servers that extend what Claude Desktop and other MCP clients can do. Before DXT, connecting Claude to external tools meant manually installing Node.js or Python and editing JSON configuration files. While not complicated for developers, this was imposing for users without a technical background. With DXT support, it's literally one-click installation with no computer science knowledge needed: 1. Download a `.dxt` file 2. Double-click to install That's it — no terminal commands, no JSON configuration files to edit. The [automatic DXT generation](/docs/standalone-mcp/build-server#desktop-extension-dxt-generation) creates a packaged Desktop Extension alongside your MCP server, making distribution to non-technical users seamless. ### Built-in Cloudflare deployment No big company has invested in MCP infrastructure like Cloudflare. The result is that Cloudflare Workers are the infrastructure of choice for remote MCP servers, which is why deploying to Workers is now a native feature of our MCP server generator. The [built-in Cloudflare deployment](/docs/standalone-mcp/cloudflare-deployment) makes getting your MCP server running on Workers incredibly simple. Just specify your Worker's URL and we'll create the necessary configuration so that deploying your server is a single command: ```bash npx wrangler deploy ``` This integration leverages Cloudflare's robust MCP infrastructure investment, giving your MCP server access to their global edge network with minimal setup effort. ### Streamlined OAuth setup The MCP specification recommends OAuth 2.1 with Dynamic Client Registration (DCR) for seamless authentication. While some APIs support DCR, the vast majority don't. Our [streamlined OAuth setup](/docs/standalone-mcp/setting-up-oauth) makes setting up authentication for your MCP server easy, regardless of your API's auth set up: - **For APIs with DCR support**: Direct integration with your existing OAuth provider - **For APIs without DCR**: Automatic OAuth proxy generation that bridges the gap between MCP requirements and your provider's capabilities You get production-ready authentication without building custom OAuth infrastructure from scratch. ## Moving on from bundled MCP If you're currently using MCP servers generated as part of TypeScript SDK generation, migrating to standalone generation is straightforward: 1. **Run standalone generation**: Use the Speakeasy CLI to generate a standalone MCP server. ```bash speakeasy quickstart --mcp ``` 2. **Update configurations**: Remove MCP server generation from your TypeScript SDK by editing the `gen.yaml` file. ```yaml filename="gen.yaml" enableMCPServer: False ``` 3. **Distribute easily**: Use your MCP server locally with DXT or deploy on Cloudflare for a remote server experience. For detailed documentation on customization, deployment, and advanced features, visit our [standalone MCP documentation](/docs/standalone-mcp/build-server). ## The benefits of unbundling Human developers (who use SDKs) and AI agents (who use MCP) have different needs — so bundling them together into one package means that suboptimal trade-offs have to be made between them. By moving MCP server generation into its own dedicated target, we can optimize for each audience individually. Here are some examples of how the standalone MCP target delivers better performance: - **Agent-friendly naming conventions**: When MCP tools are bundled in the TypeScript SDK, field names get converted to camelCase per TypeScript convention. However, this confuses LLMs since tool descriptions reference the original field names. For example, an API field called `user_name` would become `userName` in the TypeScript SDK, but tool descriptions would expect the original `user_name`. The standalone MCP server maintains the original naming format, avoiding this confusion. - **Richer data descriptions**: Every property gets detailed Zod descriptions that help AI understand your data model. Including these in an SDK would bloat the bundle unnecessarily, but they're essential for AI tools. - **Smaller SDK bundles**: Removing MCP functionality from the TypeScript SDK means reduced bundle size for web developers. - **Simplified configuration**: With MCP, there's less need to worry about style choices such as HTTP response format or union syntax that matter more for human developers. --- *The future of API-AI integration is here, and it's built for independence, optimization, and production scale. Try standalone MCP generation today and see the difference dedicated tooling makes.* # Awkward manual pagination Source: https://speakeasy.com/blog/release-terraform-globals-pagination import { Callout } from "@/lib/mdx/components"; We're excited to announce three significant improvements to Terraform provider generation that bring it closer to SDK generation parity. These features eliminate manual workarounds, reduce code duplication, and provide a cleaner developer experience for Terraform users. ## 🌐 Provider-level globals Define common configuration values once at the provider level instead of repeating them across every resource. ### What it solves Before globals support, Terraform providers required manual coding to handle provider-level configuration like region settings or organization IDs. There was no clean workaround — teams had to implement custom solutions or accept repetitive configuration across resources. Terraform consumers can choose the appropriate style of configuration for their use case: * Per-resource: Same as before. * Provider-only: Configure once at the provider level for all resource configurations. * Provider with per-resource overrides: Configure the provider level with a default value while a resource level value will override. ### How it works Globals use the same Speakeasy globals extension as SDK generation, ensuring consistency across all generated code. Simply add `x-speakeasy-globals` to your OpenAPI specification: ```yaml x-speakeasy-globals: - name: region description: The AWS region to use type: string - name: orgId description: Organization identifier type: string ``` This generates clean provider-level configuration in your Terraform blocks: ```hcl provider "myapi" { region = "us-west-2" org_id = "my-org-123" } resource "myapi_instance" "example" { # region and org_id automatically available name = "my-instance" } ``` ## 📄 Automatic pagination handling Terraform data sources now handle paginated API responses automatically behind the scenes. ### What it solves Previously, pagination handling was incomplete, requiring manual data resource creation. Paginated results are now fully handled and unnecessary pagination details are hidden from consumers. ### How it works Using the same Speakeasy pagination extension as SDK generation, providers automatically handle all pagination types: - Cursor-based pagination - Offset/limit pagination - Page-based pagination Before pagination support, users had to manually handle paging: ```hcl data "myapi_resources" "page1" { page_number = 1 page_size = 100 } data "myapi_resources" "page2" { page_number = 2 page_size = 100 } ``` After pagination support, it's seamless: ```hcl # Clean automatic pagination data "myapi_resources" "all" { # Automatically fetches all pages } # Access all results output "resource_count" { value = length(data.myapi_resources.all.items) } ``` ## 🔗 Server URL variables Configure server endpoints dynamically with support for environment-specific hostnames and region-specific endpoints. ### What it solves Previously, Terraform providers had limited server URL customization options. Teams couldn't easily switch between staging and production environments or configure region-specific endpoints without manual modifications. ### How it works Server URL variables provide the same functionality as SDK generation, with full OpenAPI server URL variable support: ```yaml servers: - url: https://{environment}.api.{region}.example.com variables: environment: enum: [staging, prod] default: prod region: enum: [us, eu, asia] default: us ``` This generates provider-level configuration with environment variable support: ```hcl provider "myapi" { # Full server URL customization is still supported # server_url = "https://staging.api.eu.example.com" # Now each server URL variable is exposed for simplified configuration. environment = "staging" region = "eu" } ``` ## 🚀 Bringing Terraform generation to parity All three features use the same extensions as SDK generation, ensuring consistency across your entire API ecosystem. This represents a significant step toward feature parity between SDK and Terraform provider generation. ### Key benefits - **DRY configuration**: Define common values once with globals - **Seamless data access**: Automatic pagination eliminates manual handling - **Environment flexibility**: Server URL variables support any deployment scenario - **Consistent tooling**: Same extensions work across SDKs and Terraform providers - **No breaking changes**: All features are additive and backward-compatible These features for Terraform provider generation are available now. Update your OpenAPI specifications with the appropriate extensions and regenerate your providers to take advantage of these improvements. ## Next steps Ready to enhance your Terraform providers? Check out our documentation for implementation details: - [Terraform provider generation](/docs/terraform/create-terraform) - [Defining globals](/docs/terraform/customize/provider-configuration#globals) - [Pagination configuration](/docs/sdks/customize/runtime/pagination) - [Server URL variables](/docs/terraform/customize/provider-configuration#server-url) These improvements eliminate common pain points and provide a more polished experience for both provider developers and end users. We're committed to continuing this progress toward full feature parity between SDK and Terraform provider generation. # API has separate project_id, region, and name fields Source: https://speakeasy.com/blog/release-terraform-jq-transformations import { CalloutCta } from "@/components/callout-cta"; Moving an existing Terraform provider to code generation isn't always straightforward. The provider you built by hand likely evolved over time with naming conventions, structural decisions, and interface choices that made sense for your users—but don't exactly match your API. Maybe you simplified tag management by using a map instead of the array-of-objects structure your API returns. Perhaps you concatenated multiple API fields into a single identifier. These thoughtful design decisions improve the user experience, but they create a challenge when migrating to a provider generated from an OpenAPI spec. We've added support for data transformations in Speakeasy's Terraform provider generator. Using JQ expressions, you can transform data between your provider's interface and your API's structure, maintaining your existing interface while moving to generated code. ## The migration challenge Existing Terraform providers often diverge from their underlying APIs in useful ways. These differences accumulate as you refine the provider based on user feedback, add convenience features, or work around API quirks. Here are common patterns that create migration challenges: ### Structural simplification Many APIs represent tags or labels as arrays of objects: ```json { "tags": [ { "key": "environment", "value": "production" }, { "key": "team", "value": "platform" }, { "key": "cost-center", "value": "engineering" } ] } ``` But from a Terraform user's perspective, this structure is unnecessarily complex. A simple map is more ergonomic: ```hcl resource "platform_instance" "main" { name = "api-server" tags = { environment = "production" team = "platform" cost-center = "engineering" } } ``` Your hand-coded provider handles this transformation internally. Moving to generation means either accepting the more complex API structure or finding a way to maintain the simplified interface. ### Composite identifiers Some APIs split related identifiers across multiple fields, but Terraform providers often combine them for convenience: ```hcl # Provider combines them into a single ID for simplicity resource "platform_database" "main" { id = "projects/my-project/regions/us-east1/databases/prod-db" } ``` The provider parses this combined identifier internally when making API calls. Generated code would naturally use separate fields unless you can specify the transformation. ### Field naming improvements API field names don't always align with Terraform conventions. Your provider might rename fields for consistency: ```hcl # API uses "displayName" but provider uses "name" # API uses "maxInstances" but provider uses "max_instance_count" resource "platform_service" "api" { name = "api-service" # API: displayName max_instance_count = 10 # API: maxInstances } ``` These naming improvements create a better experience, but they're custom logic that needs to persist through migration. ## Data transformations with JQ Data transformations use [JQ expressions](https://jqlang.org/manual/) to convert between your provider interface and your API structure. JQ is a widely-used JSON manipulation language—if you've worked with complex JSON transformations, you've likely encountered it. The transformation layer sits between Terraform and your API, automatically converting data in both directions: - **Requests**: Transform Terraform configuration values into the structure your API expects - **Responses**: Transform API responses into the structure your Terraform provider exposes ### Converting tags to a map Here's how to transform tag arrays into the simpler map structure: ```yaml requestBody: content: application/json: schema: type: object x-speakeasy-transform-to-api: jq: | .tags |= ( if type == "object" then to_entries | map({key: .key, value: .value}) else . end ) properties: tags: type: array items: type: object properties: key: type: string value: type: string ``` Users configure tags as a simple map in their Terraform configuration. The transformation converts it to the array structure before sending it to the API. Response transformations work in reverse, converting arrays back to maps. ### Building composite identifiers Create combined identifiers from separate API fields: ```yaml responses: "200": content: application/json: schema: type: object x-speakeasy-transform-from-api: jq: | .id = "projects/\(.project_id)/regions/\(.region)/databases/\(.name)" properties: project_id: type: string region: type: string name: type: string ``` The API returns three separate fields. The transformation concatenates them into a single identifier that Terraform users work with. ### Renaming fields Normalize field names to match Terraform conventions: ```yaml schema: type: object x-speakeasy-transform-to-api: jq: | { displayName: .name, maxInstances: .max_instance_count } properties: displayName: type: string maxInstances: type: integer ``` The transformation maps between your provider's naming conventions and the API's field names. ## The JQ playground Writing and testing JQ expressions requires iteration. We've built a playground specifically for Terraform provider transformations at [jq.speakeasy.com](https://jq.speakeasy.com). The playground includes: - Syntax highlighting and validation - Real-time transformation preview - Example transformations for common patterns - Error messages that explain what went wrong Use the playground to develop transformations before adding them to your OpenAPI specification. Test with sample data to verify the transformation produces the expected structure in both directions. ## Beyond migration: forward-looking transformations While migration is the primary use case, data transformations also support forward-looking changes. If you know your API will change in the future, you can implement the new structure in your Terraform provider ahead of time. For example, suppose your API currently uses a field name you plan to deprecate: ```yaml # API currently uses 'fubar' but you're migrating to 'configuration' schema: type: object x-speakeasy-transform-to-api: jq: | { fubar: .configuration } properties: configuration: type: object ``` Your Terraform provider exposes the new `configuration` field name while the API still expects `fubar`. When the API changes, you remove the transformation rather than forcing a breaking change on Terraform users. This technique works for any planned API evolution where you want to minimize downstream impact. ## When to use transformations Data transformations add complexity. They're valuable when: - **Migrating existing providers**: Maintain your current interface while moving to generation - **Significant UX improvements**: The transformation provides a meaningfully better user experience (like tags as maps) - **Planned API changes**: You're preparing for a known future API modification Avoid transformations when: - **The API structure is reasonable**: If your API's structure works well in Terraform, don't add unnecessary transformation logic - **Simple renames**: Minor naming differences usually aren't worth the complexity - **You can change the API**: If you control the API and it's not yet released, consider changing the API instead Generated code that directly reflects your API is simpler and easier to maintain. Use transformations when the value clearly outweighs the added complexity. ## Next steps Data transformations are available now for Terraform provider generation. If you're considering migrating an existing provider or need to bridge the gap between your API structure and ideal provider interface, check out the documentation: - [Terraform provider generation](/docs/terraform/create-terraform) - [JQ playground](https://jq.speakeasy.com) Data transformations solve a specific problem: they let you maintain the provider interface you've carefully designed while moving to generated code. This bridges the gap between manual implementation and generation, making migration feasible even when your provider has evolved significantly from your API's structure. # Configure the cloud management provider Source: https://speakeasy.com/blog/release-terraform-per-resources Large infrastructure companies often expose multiple APIs under a single platform: a cloud management API for creating and configuring resources, plus specialized APIs for managing individual databases, clusters, or other infrastructure components. Each API may have its own endpoint and credentials. Terraform's configuration model presents a challenge here. It provides only two levels of configuration: global provider settings and individual resource settings. There's no concept of grouping related resources with shared configuration. This limitation meant you either needed to create separate Terraform providers for each API or write significant custom code to handle the complexity. We've added support for per-resource security and server URLs in Terraform provider generation. This feature allows resource groups to declare their own endpoints and credentials while staying within a single provider. ## The challenge with multi-API infrastructure platforms Infrastructure platforms often separate their control plane APIs from their data plane APIs. Here's a common pattern: - **Cloud API**: Manages users, organizations, clusters, and high-level resources at `https://api.platform.com` - **Cluster API**: Manages resources within a specific cluster at `https://cluster-abc123.platform.com` with cluster-specific credentials Take MongoDB Atlas as a real-world example. The Atlas Admin API handles projects, clusters, and organizational resources. But once a cluster exists, managing databases, collections, and users within that cluster requires connecting to the cluster's specific API endpoint with database-specific credentials. ### The traditional approach: multiple providers The technically correct solution in Terraform is to create separate providers: ```hcl provider "platform_cloud" { api_token = var.cloud_token api_url = "https://api.platform.com" } # Create a cluster using the cloud provider resource "platform_cloud_cluster" "main" { name = "production" } # Configure a separate provider for cluster management provider "platform_cluster" { api_token = var.cluster_token server_url = platform_cloud_cluster.main.endpoint } # Manage resources within the cluster resource "platform_cluster_topic" "orders" { provider = platform_cluster name = "orders" } ``` This works, but requires: - Maintaining two separate Terraform providers - Users downloading and managing multiple provider binaries - Coordination between two codebases for API changes - Complex provider aliasing in Terraform configurations ## Per-resource configuration With per-resource security and server URL support, resources can accept their own endpoint and credentials directly: ```hcl provider "platform" { api_token = var.cloud_token } # Create a cluster resource "platform_cluster" "main" { name = "production" } # Configure resources within the cluster - no second provider needed resource "platform_cluster_topic" "orders" { name = "orders" server_url = platform_cluster.main.endpoint api_token = var.cluster_token } ``` The cluster topic resource accepts its own `server_url` and `api_token` configuration, pulling values directly from the cluster resource output. Users write a single Terraform configuration that handles both layers without managing multiple providers. ## How it works Per-resource configuration uses the OpenAPI specification's operation-level security and server definitions. When your OpenAPI spec declares different servers or security requirements for specific operations, Speakeasy can expose these as resource-level configuration options in the Terraform provider. This feature is disabled by default. Not every API with operation-level configuration needs per-resource settings exposed to end users. Enable it selectively for resources that genuinely require different endpoints or credentials. ### When to use per-resource configuration This feature fits specific architectural patterns: - **Admin APIs and data APIs**: Separate management planes and control planes with distinct authentication - **Multi-tenant infrastructure**: Resources that connect to tenant-specific endpoints - **Cluster-based systems**: Managing both cluster provisioning and internal cluster resources - **Database platforms**: Controlling database instances plus in-database resources like schemas and users ## Configuration from a user perspective Users configure per-resource settings just like any other resource attribute. The difference is these settings control how the provider connects to the API rather than what data gets sent. Here's what it looks like with environment-specific clusters: ```hcl resource "platform_cluster" "staging" { name = "staging" } resource "platform_cluster" "production" { name = "production" } # Same resource type, different endpoints resource "platform_cluster_topic" "staging_orders" { name = "orders" server_url = platform_cluster.staging.endpoint api_token = var.staging_cluster_token } resource "platform_cluster_topic" "production_orders" { name = "orders" server_url = platform_cluster.production.endpoint api_token = var.production_cluster_token } ``` The provider handles the different endpoints automatically based on the configuration. Users don't need provider aliases or complex meta-arguments. ## Next steps Per-resource security and server URL support is available now for Terraform provider generation. When your OpenAPI specification includes operation-level servers or security schemes, you can choose which resources should expose these as configuration options. If you're building providers for infrastructure platforms with multiple API layers, or migrating existing providers with these patterns, check out the documentation: - [Terraform provider generation](/docs/terraform/create-terraform) - [Provider configuration](/docs/terraform/customize/provider-configuration) # release-terraform-polling Source: https://speakeasy.com/blog/release-terraform-polling Long running requests are a pattern that appears throughout infrastructure APIs. That's why Terraform providers generated with Speakeasy will now include automatic polling support for API operations that perform background processing. This eliminates the need to manually implement waiter logic when resources need time to reach a desired state. ## ⏱️ Automatic polling for long-running operations Many infrastructure API operations perform work asynchronously. When creating a database instance, spinning up compute resources, or processing batch jobs, the API returns immediately with an identifier, but the actual work happens in the background. The resource must repeatedly poll a status endpoint until the operation completes. Before polling support, Terraform provider developers had to manually implement waiter logic for each resource: ```go // Manual waiter implementation in Terraform provider code func (r *DatabaseResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { var data DatabaseResourceModel resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...) if resp.Diagnostics.HasError() { return } client := r.client // Create the resource instance, err := client.CreateDatabase(ctx, data) if err != nil { resp.Diagnostics.AddError("Client Error", fmt.Sprintf("Unable to create database, got error: %s", err)) return } data.ID = types.StringValue(instance.ID) // Manual polling loop timeout := time.After(10 * time.Minute) ticker := time.NewTicker(5 * time.Second) defer ticker.Stop() for { select { case <-timeout: resp.Diagnostics.AddError("Timeout", "timeout waiting for database to be ready") return case <-ticker.C: status, err := client.GetDatabase(ctx, instance.ID) if err != nil { resp.Diagnostics.AddError("Client Error", fmt.Sprintf("Unable to get database status: %s", err)) return } if status.State == "ready" { resp.Diagnostics.Append(resp.State.Set(ctx, &data)...) return } if status.State == "failed" { resp.Diagnostics.AddError("Database Creation Failed", "database creation failed") return } } } } ``` This pattern appears across infrastructure providers: databases, compute instances, networking resources, and any API that performs background work. Each resource requires careful handling of timeouts, intervals, success conditions, and failure states. ### How it works Polling support uses the `x-speakeasy-polling` OpenAPI extension to define success criteria, failure criteria, and timing configuration for operations: ```yaml paths: /databases/{id}: get: operationId: getDatabase x-speakeasy-polling: - name: WaitForCompleted delaySeconds: 2 intervalSeconds: 5 limitCount: 120 successCriteria: - condition: $statusCode == 200 - condition: $response.body#/state == "ready" failureCriteria: - condition: $statusCode == 200 - condition: $response.body#/state == "failed" ``` #### Configuration options Each polling option supports: - **delaySeconds**: Initial delay before first poll (default: 1) - **intervalSeconds**: Time between subsequent polls (default: 1) - **limitCount**: Maximum number of polling attempts (default: 60) - **successCriteria**: Conditions that indicate operation completion (using [Arazzo criterion objects](https://spec.openapis.org/arazzo/latest.html#criterion-object)) - **failureCriteria**: Conditions that indicate operation failure (using Arazzo criterion objects) Multiple polling options can be defined for the same operation, each with different success criteria for different use cases. ### Entity operation configuration To wire polling configurations to Terraform resources, you'll also need to configure the `x-speakeasy-entity-operation` extension. This tells the generator how and when to use the polling configuration for each resource operation: ```yaml /task: post: x-speakeasy-entity-operation: Task#create#1 /task/{id}: get: responses: "200": description: OK content: application/json: schema: type: object properties: name: type: string status: type: string enum: - completed - errored - pending - running required: - name - status x-speakeasy-polling: - name: WaitForCompleted failureCriteria: - condition: $statusCode == 200 - condition: $response.body#/status == "errored" successCriteria: - condition: $statusCode == 200 - condition: $response.body#/status == "completed" x-speakeasy-entity-operation: - Task#read - entityOperation: Task#create#2 options: polling: name: WaitForCompleted ``` This configuration provides the flexibility to use different polling settings for different operations on the same resource. For example, creation might need a delay before starting polls and take longer to complete, while deletion can start immediately and finish quicker. For more details on entity operation configuration, see our [entity mapping documentation](https://www.speakeasy.com/docs/terraform/customize/entity-mapping#api-operation-polling). ### Terraform resource behavior After defining polling configuration in your OpenAPI specification, Terraform resources automatically handle state transitions: ```hcl resource "myapi_database" "example" { name = "production-db" region = "us-west-2" # Terraform automatically waits for the database to be ready # using the polling configuration from your OpenAPI spec } ``` When a resource is created, updated, or deleted, Terraform uses the polling configuration to wait for the operation to complete before proceeding. This provides a seamless experience for Terraform users without requiring custom waiter implementations in provider code. #### Resource lifecycle integration Polling integrates with all phases of the Terraform resource lifecycle: - **Create**: Wait for new resources to reach a ready state - **Update**: Wait for modifications to be applied - **Delete**: Wait for resources to be fully removed - **Read/Refresh**: Ensure resources are in expected states during plan operations ### Error handling When polling encounters issues, Terraform receives clear error messages: - **Failure criteria met**: If the API indicates the operation failed (status reaches "failed", "error", etc.), Terraform immediately stops and reports the failure - **Timeout reached**: If polling exceeds the configured limit count, Terraform reports a timeout error with context about how many attempts were made This allows Terraform users to understand what went wrong and take appropriate action, whether that's investigating API issues or modifying resource configuration. ## 🚀 Eliminating manual waiter logic Polling support eliminates one of the most tedious aspects of Terraform provider development. By embedding this pattern directly in generated providers, Speakeasy removes the need for custom waiter implementations. ### Key benefits - **Eliminate boilerplate**: No manual polling loops in Terraform provider code - **Consistent behavior**: Same polling pattern across all resources - **Flexible configuration**: Define timing and criteria in OpenAPI specifications - **Proper error handling**: Distinguish between operation failures and timeouts - **Seamless user experience**: Terraform resources "just work" with long-running operations - **Declarative configuration**: Polling behavior lives in API specifications, not code ## Next steps Ready to add polling support to your Terraform providers? Check out our documentation: - [Terraform provider generation](/docs/terraform/create-terraform) Polling support works alongside other Speakeasy features like retries, globals, pagination, and authentication to provide a complete Terraform provider generation solution. # Before: Traditional pip installation Source: https://speakeasy.com/blog/release-uv-python Python developers are rapidly adopting [UV](https://github.com/astral-sh/uv), and now Speakeasy Python SDKs support it too. UV is a next-generation package manager written in Rust that delivers 10-100x faster performance than traditional Python tooling. Starting today, all new Python SDKs generated by Speakeasy use UV by default, while maintaining full backward compatibility with Poetry for existing projects. ## What is UV? UV is an extremely fast Python package and project manager created by [Astral](https://astral.sh), the team behind the popular Ruff linter. Built in Rust, UV serves as a single tool to replace `pip`, `pip-tools`, `pipx`, `poetry`, `pyenv`, `twine`, `virtualenv`, and more. The tool has gained significant traction in the Python community for its speed and simplified developer experience. Instead of juggling multiple Python tools, developers can use UV as their Swiss Army knife for all package management tasks. ## Dramatic performance improvements The most immediate benefit is speed. UV delivers 10-100x faster package operations compared to pip, with particularly significant improvements for: - **Initial SDK installation**: Installing a Python SDK for the first time - **Dependency updates**: Regenerating SDKs with updated dependencies - **Custom modifications**: Adding additional packages to customized SDKs This performance boost is especially noticeable for complex SDKs with many dependencies, where installation time drops from minutes to seconds. ```bash pip install acme-sdk # Takes 2-3 minutes for complex SDKs # After: UV-powered installation uv add acme-sdk # Completes in 10-20 seconds ``` ## Simplified developer experience UV consolidates the Python toolchain into a single, intuitive interface. Where developers previously needed to coordinate multiple tools, UV handles everything: - **Package installation and removal** - **Virtual environment management** - **Dependency resolution and locking** - **Project publishing** - **Python version management** This unification reduces the cognitive overhead of working with Python SDKs, making them easier to integrate and maintain in projects. ## Backward compatibility maintained Existing Python SDK users can continue using Poetry without any changes. The transition to UV is opt-in, ensuring zero disruption to current workflows. Both package managers are fully supported: ```yaml # Continue using Poetry python: packageManager: poetry # Or switch to UV for faster performance python: packageManager: uv ``` ## Migration is straightforward For teams ready to experience UV's performance benefits, migration is simple. simply update your SDK's `gen.yaml` file to use UV as the package manager: ```yaml python: ... packageManager: uv ``` # release-webhooks-support Source: https://speakeasy.com/blog/release-webhooks-support We're excited to announce that Speakeasy SDK generation now supports webhooks. This new feature makes it dramatically easier for both API producers and consumers to work with webhooks by providing type-safe handlers and built-in security verification, all powered by OpenAPI-native features. ## The webhook integration challenge If you maintain an API platform, you almost certainly have webhooks. But implementing webhook support has always been more complex than it needs to be, with challenges on both sides of the integration: For API consumers, integrating webhooks typically involves: - Implementing complex security verification from scratch - Manual type construction and validation - Handling undefined or unexpected payloads - Managing authentication secrets - Building resilient processing infrastructure For API producers: - Writing extensive documentation for webhook security - Maintaining separate authentication systems - Ensuring correct payload construction - Managing webhook delivery and retries - Supporting customers through integration challenges The result? Developers spend more time wrestling to integrate with the webhook system than building product on top of it. ## Better webhooks for producers & consumers Our new webhook support simplifies this entire process by generating typed webhook handlers with built-in security verification. Here's what that looks like in practice. ### Consumer Support ```typescript const res = await sdk.validateWebhook({ request, secret }); if (res.error) { throw res.error; // Failed auth or unknown payload } const data = res.data; if (data.type === "company.created") { return myHandleCompanyCreated(data); } if (data.type === "company.deleted") { return myHandleCompanyDeleted(data); } if (data.type === "connection.created") { return myHandleConnection(data); } throw new Error(`Unhandled webhook payload: ${res.inferredType}`); ``` No more implementing HMAC SHA-256 verification from scratch. No more debugging undefined properties in production. Just install the SDK and start processing webhooks with confidence. ### Producer Support Generate convenience methods for producers to correctly construct and sign webhook payloads. ```typescript filename="sendPayment.ts" const finotech = new Finotech(); const result = await finotech.webhooks.sendCompanyCreated({ recipient: { url: "https://bank.com/finotech-webhooks", secret: "secret-key" }, { id: "foo", name: "Foo Inc" } }); ``` ### Key Features **Type-Safe**: All webhook payloads are fully typed, letting you catch integration issues at compile time rather than production. **Built-in Security**: Support for industry-standard webhook security including HMAC SHA-256 signing, with more verification methods coming soon. **OpenAPI Native**: Webhooks are defined right in your OpenAPI spec, ensuring your SDK, docs, and webhook interfaces stay in sync as your API evolves. ## Getting Started The magic happens through OpenAPI's native webhook support. Simply add add our custom extension to configure your webhook security and your SDK will generate the rest: ```yaml filename="openapi.yaml" openapi: 3.1.1 paths: ... x-speakeasy-webhooks: security: type: signature # a preset which signs the request body with HMAC name: x-signature # the name of the header encoding: base64 # encoding of the signature in the header algorithm: hmac-sha256 webhooks: user.created: post: requestBody: required: true content: application/json: schema: type: object properties: id: type: string required: - id responses: "200": description: Okay ``` The SDK will sign the request body. More complex signing schemes are also supported. Get in touch to find out how to support your signing scheme. ## Webhooks maturity Webhook support is available today in TypeScript, with support for additional languages rolling out soon. We've also got some exciting features on our roadmap: - Asymmetric signature verification - Key rotation support - Custom signing scheme support - Infrastructure for reliable webhook delivery and processing For existing Speakeasy Enterprise customers, define `webhooks` in your OpenAPI spec and we'll automatically include webhook generation in your next action run. For new customers, you can start generating webhook-enabled SDKs today by [signing up](https://app.speakeasy.com/). Join the growing number of companies using Speakeasy to deliver exceptional API experiences, now with first-class webhook support! # release-zod-v4 Source: https://speakeasy.com/blog/release-zod-v4 import { Callout, Table } from "@/mdx/components"; Zod v4 mini support is available now in all Speakeasy TypeScript SDKs. Enable it by modifying the `zodVersion: v4-mini` flag to your gen.yaml configuration. Today we're releasing support for Zod v4 mini in all Speakeasy-generated TypeScript SDKs. This update brings bundle size reductions of ~10% while maintaining the runtime type safety that makes our SDKs reliable. ## Why Zod matters When we [rebuilt our TypeScript generator](/post/how-we-built-universal-ts), Zod was a critical piece of the puzzle. It solved a fundamental problem: TypeScript has no runtime type safety. Types exist only at compile time, disappearing entirely when your code runs. This creates real issues when working with APIs. Without runtime validation, there's no guarantee that: - The data you send matches your API's requirements - The responses you receive match your expectations - Type mismatches get caught before they cause bugs Zod gives us runtime type safety by validating API responses before your code uses them: ```typescript import { SDK } from "@speakeasy/ecommerce-sdk"; async function run() { const sdk = new SDK(); const products = await sdk.products.list(); // Without Zod validation: // ❌ Runtime error: "Cannot read properties of undefined (reading 'amount')" console.log(product.price.amount); } // With Zod validation, fails fast with a clear error: // ZodError: Required at path: ["product", "price"] ``` This catches API contract mismatches immediately with clear errors, rather than letting your code crash when it tries to access properties that don't exist. ## Every improvement counts We're always asking ourselves: can we make our SDKs better? When Zod v4 was released, we saw an opportunity to improve bundle efficiency. Zod v3 has served us well. In a typical SDK, it accounts for around 15% of the bundle size—about 20KB minified and gzipped. For many applications, this is a reasonable tradeoff for runtime type safety. But with Zod v4 mini offering better tree-shaking through its standalone function API, we could reduce that footprint even further without compromising on validation. ## Enter Zod v4 mini Zod v4 introduced a new variant called "mini" that fundamentally changes how validation works. Instead of method chaining, it uses standalone functions: ```typescript // Zod v3/v4 (method chaining) const schema = z.string().optional().nullable(); // Zod v4 mini (standalone functions) const schema = z.nullable(z.optional(z.string())); ``` This might look like a cosmetic change, but it enables better tree-shaking. Standalone functions are module-level exports that bundlers can analyze and eliminate when unused. A class-based API, by contrast, pulls in the entire class structure regardless of which methods you actually call. Zod v4 mini clocks in at around 10KB minified and gzipped—about half the size of Zod v3's 20KB. Beyond bundle size, Zod v4 delivers significant performance improvements: - String parsing is 14.7x faster - Array parsing is 7.4x faster - Object parsing is 6.5x faster ## How this impacts Speakeasy SDKs These library changes are entirely internal to our generated SDKs. Your SDK users don't need to change anything—they continue using the same class-based interface they're familiar with: ```typescript import { Dub } from "dub"; const dub = new Dub({ token: process.env.DUB_API_KEY }); const result = await dub.links.create({ url: "https://example.com", domain: "dub.sh", }); ``` Under the hood, we've migrated all validation logic to use Zod v4 mini's standalone functions. Here's what we're seeing in real-world SDKs:
Your SDK users automatically benefit from: - Smaller bundle sizes when building frontend applications (typically ~10% reduction) - Better tree-shaking that only includes validation for the SDK methods they actually use - Faster validation at runtime We've also maintained backward compatibility. Thanks to [the forward thinking zod team](https://zod.dev/library-authors?id=how-to-support-zod-3-and-zod-4-simultaneously) our SDKs work regardless of whether v3 or v4 is installed in your project. ## Getting started Zod v4 mini support is opt-in via your gen.yaml configuration file. Add the zodVersion flag to your TypeScript generation settings: ```yaml typescript: version: 1.0.0 zodVersion: v4-mini ``` Then regenerate your SDK: ```bash speakeasy run ``` Some edge cases may require sticking with Zod v3. For example, MCP servers currently have compatibility issues with Zod v4. We recommend testing the migration in a development environment before deploying to production. ## Looking forward Zod v4 mini is part of our broader commitment to building TypeScript SDKs that work everywhere: from Node.js servers, to browser frontends to edge functions. Every improvement matters, especially in bundle-sensitive environments like edge functions and mobile applications. If you're generating TypeScript SDKs with Speakeasy, we encourage you to try Zod v4 mini. The migration is straightforward, and you'll benefit from smaller bundles and faster validation. # replay-requests Source: https://speakeasy.com/blog/replay-requests ## New Features - **Request Replay** - It can be difficult to reproduce API issues that users are seeing in production: there's so many variables that need to be accounted for. Now you can use the request viewer to find and replay the exact request you need. All the headers and request objects are editable so that you can make sure you have a high fidelity replica to test with. [Speakeasy Request Replays - Watch Video](https://www.loom.com/share/07b41e6d6f5b406a8853947d77487cc1) ## Smaller Changes - **OpenAPI Schema Validation - Catch errors in your OpenAPI schema before your clients do. If you upload an OpenAPI spec, Speakeasy will run a validation before applying it to your managed APIs. It will inform you of any errors in the spec, so you can address them.** - **\[self-hosting\] Ambassador Support** - Speakeasy self-hosting now supports clients using Ambassador. Just configure the values.yaml file to get set up. # Transcript Source: https://speakeasy.com/blog/request-response-anuj-jhunjhunwala import { NewsletterSignUp } from "@/components/newsletter-signup"; import { anujJhunjhunwalaEpisode } from "@/lib/data/request-response"; import { PodcastCard } from '@/components/podcast';
[Anuj Jhunjhunwala](https://www.linkedin.com/in/anuj-jhunjhunwala/) is the Director of Product and Design at [Merge](https://merge.dev/). In this episode, Anuj shares how Merge is transforming integration pain into product velocity with their unified API approach. We discuss: - The pain of building and maintaining custom integrations - How AI is making integrations table stakes for modern products - The three types of data that matter for AI applications - Why proprietary data accessed through integrations creates competitive differentiation - How AI is changing API design expectations and consumption patterns - The evolution from traditional CRUD to richer, contextual queries - What great developer experience really means in practice ### Listen on [Apple Podcasts](https://podcasts.apple.com/us/podcast/ais-impact-on-api-integration-patterns-and-building/id1801805306?i=1000713359298) | [Spotify](https://open.spotify.com/episode/247AHOYYfswDokKxruYk7F)
## Show Notes [02:30] – The pain of building and maintaining custom integrations [03:45] – Merge as "Plaid for B2B software" and the magic moment for devs [07:30] – AI making integrations table stakes; the three types of data [08:30] – Why proprietary data is the key to differentiation in AI [09:45] – AI and the future of APIs: personalization and intuitive design [11:30] – DX vs AX: API design for devs and for AI [12:00] – How AI product patterns are changing API requirements [13:00] – Richer queries, delta endpoints, and evolving API design [14:00] – The rising importance of fine-grained permissions [15:00] – HubSpot, search endpoints, and future-facing API choices [16:00] – Delta endpoints explained and why they're valuable for LLMs [17:00] – Principles of great developer experience: predictability and frictionlessness [19:00] – Where to learn more about Merge ## Key Quotes From The Discussion ### API Integrations are Table Stakes for AI > "The biggest trend is that it's just becoming table stakes, API integrations. We talk to a lot of AI companies. We get a lot of interest in AI companies, and there's really like three types of data that is helpful when you're building an LLM, right? There's the public data that exists out there that's scraped from the internet and is publicly accessible. There's synthetic data, which is obviously produced itself by an algorithm or by an LLM and can help you validate and test out edge cases. And then there's proprietary data, right? So it's the data that belongs to your customer." ### The Magic of Proprietary Data > "The proprietary data is data that's specific to you and makes you different, and it's a thing that no one else can get access to because it literally just belongs to you or your customer. That I think, is the magic of the integration, is that the integrations pull in that third bucket and like they can make your product different, so you're leaving money on the table if you don't have an integration strategy because you're not thinking about that third bucket, which actually makes you different." ### AI's Impact on API Design and Documentation > "APIs in general just have to be extremely intuitive and easy to get started, right? So like AI, I think, spoils us at a very high level into expecting a lot of personalization, right? When you go to ChatGPT and you ask it a question, it can feel like a very pointed personal response to what you just said. It's a unique answer in many cases, like the exact question you have. Separately, but also relatedly, no one reads docs, right? No one reads anything." ### What is Great Developer Experience? > "I think ultimately what this boils down to is that great DevEx is reducing friction to the point where your team can spend its time on other high-leverage things that you'd rather spend your time on, right? That's ultimately what this is—providing leverage. It's multiplying your team. It's a force multiplier, and I think that's super powerful just because out of the box you click a few buttons on a website and it can do that for you, which is a really powerful experience in my mind." ## Referenced - [Merge.dev](https://merge.dev/) - [Plaid](https://plaid.com/) - [HubSpot](https://www.hubspot.com/) - [ChatGPT](https://chatgpt.com/) Production by [Shapeshift](https://shapeshift.so). For inquiries about guesting on Request // Response, email [samantha.wen@speakeasy.com](mailto:samantha.wen@speakeasy.com). ## Frequently Asked Questions ### What is Merge and what does it do? Merge is a unified API platform that makes it easy to add hundreds of integrations to your product. Think of it as "Plaid for B2B software" - they span HR, accounting, ticketing, file storage, CRM, and other categories. Instead of building individual integrations to tools like SharePoint, NetSuite, or Jira, you integrate once with Merge's API and get access to all their integrations. ### Why do companies choose Merge over building integrations themselves? Building custom integrations takes weeks or months per integration, requires ongoing maintenance, and can become a blocker when customers need integrations you haven't built yet. Merge eliminates this by providing pre-built integrations that are maintained by their team, allowing companies to focus on their core product instead of integration infrastructure. ### How is AI changing the importance of API integrations? AI is making integrations table stakes because of the three types of data useful for LLMs: public data (accessible to everyone), synthetic data (generated by algorithms), and proprietary data (unique to your customers). Proprietary data accessed through integrations is what creates competitive differentiation, making integration strategy critical for AI companies. ### What are delta endpoints and why do they matter? Delta endpoints return only what has changed in a database over a specific time period, rather than requiring you to iterate through all records to find updates. This is particularly valuable for AI applications that need to efficiently process large amounts of data and understand what's new or changed. ### What makes for great developer experience according to Anuj? Great developer experience is about reducing friction to the point where teams can spend time on high-leverage activities. It should be predictable, stable, frictionless to get started, and act as a force multiplier that gives developers superpowers by handling complex tasks seamlessly. ### How is AI changing API design expectations? AI is creating expectations for highly personalized, intuitive experiences similar to ChatGPT. APIs need to be extremely easy to understand and use, as people increasingly rely on AI tools instead of reading documentation. This is driving the need for more intuitive API design and better out-of-the-box experiences. [00:00:00] Great Dev X is reducing friction to the point where your team can spend its time on other high leverage things that you'd rather spend your time on. Hey everyone. Welcome to Request Response. I'm your host Ser CEO, and co-founder of Speakeasy. We are the modern tool chain for REST API development. I'm joined today by an Who runs the product and design Teams at Merge. Great to have you here today. Great day's going. Great. Thanks for having me on. I'm excited to chat. Yeah. I had the, um, opportunity to visit your office, uh, last year. I think it's really interesting Merge holds this, uh, place in the API ecosystem. As you know. I think one of the first companies that really, um, popularized being an API company and not, not a company selling an API necessarily, but actually. Building a product or tool for other API companies. So really excited to chat. Just before we get into all the, all the fun stuff happening in the a p world today, I love [00:01:00] for you to share about. What, what merge actually does today, what your role is, how you, how you ended up there. Merge in a nutshell is a unified API. We make it really easy to add hundreds of integrations to your product. So think of it as like plaid for B2B software, we span hr, accounting, ticketing, file storage, CRM. There's a bunch of different categories we offer these integrations for, but let's say you want to pull from your customer's SharePoint or NetSuite or Jira. We make that very simple. So you don't have to build those integrations yourself. We build the integration for you, or do you have the integrations built? You integrate with our API and you get access to all the integrations behind the scenes. When you see the problem of building integrations, you're like, I need this. This is one of those things that just saves me time and effort, not only in the initial build, but it also helps with maintenance, right? Because over the long run, these integrations break third party providers, change the endpoint. There's things that happen and we just handle all, all that for you, including the observability. So. It can save you a lot of time and energy. What I do here is I, I run the product and design team. It's a series B startup. We are growing rapidly. We're about [00:02:00] 115 people, uh, spread across New York, SF and Berlin. There's a lot that goes with that, right? There's the, um, ambiguity of how do you go from series B to series C. There's a lot of exciting things that we want to build a lot of. Uh, great customer feedback we get and prospect feedback, and it's just balancing all these things and making sure we're building the right things at the right time, which is a, a very, uh, exciting challenge to be working on. Yeah, that's super cool. Such a unique. Place to be as, as a company, you know, as someone who's integrated a lot of APIs myself, as a developer, why do you think people come to merge? Like as a, as a developer, is there like a singular pain point, a singular kind of magic moment that, uh, would make me. So like, Hey, I would rather use Merge than go do all these integrations myself. It's probably easiest to like use an example. So let's say you're a PM at a financial forecasting company. Like your, your job, your product is to help your customers with their financial forecasting. And so what you need to do that is you need. Your customers to somehow provide their financial data, their transactions, their journal entries, all this information that lives in [00:03:00] NetSuite or QBO or Sage, right? It, it's all in these accounting tools. And what you could do as a PM for this company is build an integration to all these different third party providers. You could have your customers upload the data, but no one wants to do that, right? No one's gonna sit there and. You know, fiddle with CSVs and all that. So, um, neither of these options are particularly enticing because to build your own integrations, each one takes weeks, months of time to do. Docs can be arcane and opaque. It's hard to understand exactly which things mo a map to what some of these integrations have, multiple APIs that just, you know, it's unclear how they kind of play together with each other. And you have to figure out things like authentication and pagination and rate limits and, you know, again, like these things can change over time as well. So you have to constantly maintain them. Keep an eye on things. You come to us and you integrate straight With Merge, you integrate with our API and behind the scenes you have access to all these different accounting integrations and your customers have this little popup box, kind of similar to Plaid, where if you go to like TurboTax and try to log into your, or pull your bank information in, your customers can come to your portal. Click on a button [00:04:00] that says, connect my accounting integration, or connect to my accounting software and pick the software that they, they use authenticate, and that's it. And to extend this even further, it's not just accounting, right? So there's, you know, this financial forecasting example, maybe you want compensation data or headcount spend and you need that from the HR platforms. Or you need pipeline data to understand like how your sales are doing. So you wanna pull that from Salesforce or HubSpot customer tickets. So you wanna analyze turn risks, so you want ticketing data. All of a sudden you're not just building like four or five integrations, you're building 50 integrations. What's tricky here and what people don't fully see all the time, is the fact that if you sign a customer as the PM at this financial forecasting company that needs some accounting software, you have not already integrated with. That becomes a blocker for you, right? Because all of a sudden you need to get their data, you need to build that integration. So what Merge does here, and what's really beneficial is not only do we provide this all outta the box for you, we have built all these integrations in-house. So. If you have that additional new customer who, who signs on chances are we support it and it's just a click of [00:05:00] the button to enable it for them. Yeah, that's really interesting. I think as you were saying, that's something that came to my mind as as a developer, is like when you build integration, it's now this live thing, right? It's this thing that needs maintenance. You need to kind of constantly put. Cycles velocity into it to keep it up to date, keep it healthy, keep it monitored, uptime, all that stuff. And it just kind of occurred to me that, you know, if I have to do that, you know, end times, as you said, instead I go to you, that's a company that's pouring all that, all those cycles into one API. Right? Your unified API. And I get probably many hundreds of dev years of time spent into that one API. It's just something that I've been thinking about a lot as, as also someone, you know who runs a company and we we're a vendor to other companies. How do you explain the value of having someone. Hyper-focused on that problem for you. And I think in your case, it, it is really apparent because you are aggregating many APIs and making it one. And so it's so obvious that like those hundreds of APIs, the, the time you take to integrate all [00:06:00] of them, the health and upkeep of all of that now just compounded. It's like kind of like compounding interest just built into one API. So that's, it's a really fascinating, uh, business model that you guys have. It's one of those things that really accelerates. The growth of our customers. At the end of the day, they have to spend a lot of time doing this in-house. So I guess there, there's like three options, right? There's one option, rebuild these integrations in-house, and that takes a lot of time and energy and slows down your roadmap to build other things. There's number two, which is that you don't build integrations at all, which either provides a worse customer experience for your customers, or it means that you're missing the boat. Right, because ultimately you need this data from your customers. That's what differentiates you as a business, is getting your own customer data and using something, doing something with it. Or the third option is you use Merge A Unified API that has all these integrations built outta the box and significantly speeds up your time and saves your money as a business. You guys also have such an interesting vantage point because of where you are with. The one API to rule them all. That vantage point must give you kind of very interesting perspective [00:07:00] on some of the shifts you're seeing in the API integration. I think, you know, one thing, uh, listeners and, and really everyone I work with is always asking me like, how is AI going to evolve API integrations? Like, is it going to make it so you don't need a unified API? Is it gonna make it make the problem worse? People still have to do these integrations, put them in production, right? It's easy to do, like. Pull up cursor and then make integration for fun deploy. But then that looks very different than someone running an at scale production system. So I'm curious to hear your take on, on what's going on on the ground. The biggest trend is that it's just becoming tables stakes, staff integrations. We talk to a lot of AI companies. We get a lot of interest in AI companies, and there's really like three types of data. That is helpful when you're building an LLM, right? There's the public data that exists out there that's scraped from the internet and is publicly accessible. There's synthetic data, which is, you know, obviously it produced itself by an algorithm or by an LLM and can help you validate and test out edge cases. And then there's proprietary data, right? So it's the data that belongs to your customer. [00:08:00] And the first two things there, the public and the synthetic data. Are accessible to basically anybody. The proprietary data is a data that's specific to you and makes you different, and it's a thing that no one else can get access to because it literally just belongs to you or your customer. That I think, is the magic of the integration, is that the integrations pull in that third bucket and like they can make your product different, so you're leaving money on the table if you don't have an integration strategy because you're not thinking about that third bucket, which actually makes you different. Maybe more broadly, the role of AI in changing how APIs are consumed and built. APIs in general just have to be extremely intuitive. And, uh, easy to get started, right? So like AI, I think, spoils us at a very high level into expecting a lot of personalization, right? When you like go to chat BT and you ask it a question, it can feel like a very pointed personal response to what you just said. It's, it's a unique answer in many cases, like the exact question you have separately, but also relatedly, no one reads docs, right? No one reads anything. Um, they, you know, they just, uh, I'm at the point where if I use a third party tool or if I am. Figuring out how to troubleshoot something at home. The first thing I do is open up chat GBT [00:09:00] and like take a picture of it and say, how do I use this thing? And so I think people are, they're using AI to personalize the type of responses they get. They expect low friction in all these encounters. And so in the future you're gonna get to a world where no one's gonna read draw docs. What they're gonna do is they're gonna go to Chacha bt and say, how do I do this thing? And so it's on, it's incumbent. It's easy, it's it. Absolutely imperative that as a business you make it very simple to understand what you do and that extends to the API. The API needs to be easy to understand. It itself is a product, so people need to understand like what the different endpoints do. Why are they using this API, what's the advantage of it? And it can't be this like very non-standard thing that makes it hard to actually build an integration on top of. So that's kind of like a maybe broader point on just like how people, how I see ai. Changing the API I landscape over time or ways that products can differentiate today in the world of AI is by having access to more data, right? Like you said, and integrations are the way to get that. So I think that's, that's a really great take because that's a, that's a really strong tailwind for unified APIs and API integrations more generally. It's kind of moves them from bringing, I [00:10:00] guess, something further down the task list to table stakes, right? Like now you just need to integrate with. As many systems as you can get, as much data. Make sure that any AI experience you have in your product has maximum access to different data behind different services. So that, that really resonates with me. I think the other interesting thing you said is like a lot of APIs are just not intuitive to use. Humans really struggle with that. Maybe AI can help us there, like actually do that process of like reading docs, understanding how to use this, the gaps. It's so funny because I think just earlier today I was, I made an MCP server for an API, it was an MCP server for our, um, internal API, so our own kind of admin endpoint that we have that has our customer data. So, you know, one of the things we do is like we generate SDKs for customers and I was looking at. Who was the last person we generated SDKs for web that has web hook in there. It gave me the results back, but it left off like the one customer I wanted. And then I asked, why did you leave it off? It basically said, I got confused with the pagination style of [00:11:00] this API. And I was like, that's kind of hilarious because it's probably what would've happened to a human too, and like this thing is struggling with the most non-I part of our own API. So yeah, I think just a long way of saying, I think. Something that all of this drives us towards is actually API design is even more and more important. It probably has like diverging branches. You have great API design for dx Great. API design for ax, the new hot term on Twitter X. That's really kind of an interesting take that you had that really stands out to me is. Um, a lot of this is actually tailwind for some of the things that we already do. As all of this moves forward, how do you see some of these consumption patterns changing as people build more actual real AI product applications and not, not just attempting to use AI to do integrations, but people have products that showcase to their customers energetic experience or chat experience or some other AI experience. How's that gonna like, pull or push you guys in? [00:12:00] What people want from the API integrations. I think there's three major things that come to mind. So the first one is an increase in breadth of what kind of data AI needs ultimately from these different APIs, right? So like inherently, AI has less friction in processing and using data just inherently in how AI works. Um, it's gonna need more data from more places, right? You're not limited by a human processing data. You have this machine that's doing it. It can just do more of it faster. So you can need more data from more places. I think also proprietary API data helps ground. LLMs and like avoid hallucination. So the data becomes more important to feedback into the system, just to like test out the products you're building. And the more structured and reliable that access is from more places, the better it is. So it kind of all ties together in like helping expand the breadth of the data that AI has access to. I think it also. Leads to more, I call almost like contextual queries, right? So I, I think traditional crud is kind of going to give way to richer queries. Like give me all candidates hired in the past year group by department with average time to hire, [00:13:00] right? So there's like very rich questions you can ask of an LLM. And so things like bulk fetching, delta endpoints are gonna be more important. You wanna have caching strategies to preempt that, right? So there's like different ways you wanna like set up the API itself to kind of return the data in a way that is. Tuned towards a customer in this case, uh, an AI like an LLM that needs more data faster. You're not kind of like limited by some of the old rest API design patterns, if you will. And that leads to the third one, which I think is that permissions are gonna matter a lot more. Mm-hmm. Um, right. So if you're pulling data from ticketing integration or Google Drive or SharePoint, for example, there's a larger surface area. To accidentally expose data for the wrong users. Right? And so you wanna be very careful in how that works. And that's something we take very seriously when we build these integrations. So I think those are kinda like the three big things that come to mind is like there's increased, increased breadth of the kind of data that you're looking at. The queries themselves can get richer, and that leads to changes and kind of evolution in how APIs are designed. Even a change in user consumption patterns, which is where permissions comes in. Um, and you wanna make sure that. [00:14:00] You really lock down the kind of data that any given user has access to at any given time on the build side for APIs, I think we can expect maybe a couple things that are. Better user experience related. Earlier you were saying like, we're no longer beholden to crud the way that rest APIs traditionally get built. Another kind of just anecdotes from today for me is I was, um, one of the MCPS that I use the most is HubSpot. So I don't have to cut to HubSpot ui, I can just, you know, figure out what our deal pipeline is like, what the deal stages are and, and things like that. And I found that HubSpot does actually really well with MCP. Because most of the tool calls when you peer under the hood and look at what it's doing, it's not using the, the crowd endpoints. They have a search query endpoint built into the API spec. And so the LM is opting to use that nine times outta 10 instead of like trying to do tool discovery and like actually say, you know, if I wanna find out the, the deal state for. Company Y and then this is like the deal values of doing those tool individual API [00:15:00] calls. It's actually just doing a search and letting HubSpot, I forgot, you know what, what it is. So, and I think that's kind of exactly what you brought up, right, is there are certain types of APIs that are going to get more. Probably be more used, more powerful, higher leverage. In this world of, of Chat first, or even agent first, our day to day we run into so many API specs for companies. That's such a core part of what we do. I think being able to help customers understand like design choices, how that might play out later on could be a really interesting thing to do. But you said this term, Delta endpoints. Uh, what, what is a Delta endpoint? So a Delta endpoint in a nutshell just basically returns. What has changed in the database itself, right? So if you wanted to see all the things that have changed in the past week, the records that have been updated, for example, it, it allows the API to return exactly what has changed. So you can see the, the, the delta, the changes in that. It just makes for a more efficient response. You don't have to like iterate through and see when the last modified ads, uh, timestamps have been updated. It's just a more efficient way of returning data, which is super helpful because LLMs again, like.[00:16:00] When they need more data, when they ingest more data to begin with, you wanna make it as simple as possible for them to understand what has changed in that data itself. Um, and so we're seeing kind of the rise of that endpoint, which has been very helpful on our end. And when we're, you know, integrating with these third parties when they have that endpoint, it makes things a lot easier on our end as well. Okay. That, that makes a lot of sense. I think, I've actually not worked with Delta Endpoints myself, but it's, it's, the pattern sounds very similar to. Change data capture database event streams where you start to look at like, yeah, you get what's changed and you act on that as opposed to doing queries. So yeah, I, I think the, the tech technology behind it makes a lot of sense to me. Yeah. As we come to the end of this, I wanted to also ask you again, as someone who builds this amazing product that devs gonna use to not do a bunch of other work, what does, like other particular points of view principles you develop over the time, over your time emerge on. What does great developer experience look like? If you are designing a new product, are there high level principles or philosophies you, you try to follow [00:17:00] to get to great dev? The first one that comes to mind is that we want to make things as predictable as they can be. So like the one thing you don't want to have unpredictability in or chaos is your a i integrations, right? Because it's ultimately your data, your customer's data that we're pulling in. You want it to be structured and understandable and, you know, ease of understanding is super important here. The one thing we never want to do is ship a breaking change, right? So like we want to keep things stable, easy for you to understand. If we do need to change something, we're gonna give you ample notice and you know, a heads up on why it's happening, but. We don't wanna do that that often. And I think that that kind of comes to the second point, which is a good developer experience is ultimately very frictionless, right? So you can like, sign up for an account, start testing it out, low cognitive load, right? You don't have to read a lot of docs or understand a lot of things upfront to start seeing how you can, how this tool can provide value for you. Um, it doesn't require a lot of explanation and um, I think this all kind of come together, right? It goes back to the point earlier about when you use chat, bt it's a very like. Tailored experience outta the box. You just start asking it questions and it returns [00:18:00] data to you. We're not quite there yet. I think with API integrations, there's a lot of, you still have to build the integration and, and, you know, ingest a unified API, but I think we're, we're headed towards that world where right outta the box, you sign up for an account and it makes sense. It's just like, this is what this does for me. This is why I need this. This is where, you know, the different models live and why I need to access all this data and do something with it. That makes this a very powerful user experience, I think in my mind. I think great developer experience really does have to give you kind of a, a feeling of having superpowers, right? You like, you do something, uh, it's so seamless, it's so frictionless that you've got hours back in your day and, and as a result you're like, this thing is, uh, yeah, is, is compounding value for me is like a huge point of leverage for me. So, yeah, that makes a lot of sense. I, we we're a develop experience company, so I, I always love to ask this question, kind of see how people think about it. I've heard all kinds of. Crazy things from great DevX means that you should work on an airplane offline to great DevX means, um, you don't need to talk about DevX, right? Like, it [00:19:00] just, it just works Such an important point for like products like this that we work on. I think ultimately what this boils down to is like great DevX is reducing friction to the point where your team can spend its time on other. High leverage things that you'd rather spend your time on, right? That's ultimately what this is, is providing leverage. It's multiplying your team. It's a force multiplier, and I think that's super powerful just because out of the box you click a a few buttons on a website and it can do that for you, which is a really powerful experience in my mind. Well, that brings us to kind of the end of our time here, uh, and thanks a ton for joining us today and just for everyone here who tuned in, if they wanna learn more about what you do or get in touch. Uh, where should they go? Well, thanks for having me on our website is merge.dev. You can follow me on LinkedIn as well. We have a lot of great things coming up and we're really excited to, to talk to you more. I'll be excited to have you back in the show here in a couple of months as AI evolves and we really have to see how people, how integration change. Yeah, will do. Thanks. Thanks for having me [00:20:00] on. # request-response-john-kodumal Source: https://speakeasy.com/blog/request-response-john-kodumal import { NewsletterSignUp, PodcastPlayer, YouTube } from "@/lib/mdx/components"; **[John Kodumal](https://www.linkedin.com/in/john-kodumal/)** is the co-founder and former CTO of **[LaunchDarkly](https://launchdarkly.com/)**. At LaunchDarkly, he created the feature flagging category and scaled the business to over 5000 customers. On the first episode of Request // Response, he joined us to discuss: - The power of separating deployment from release - How LD pioneered the use of server-sent events, long before it became the tech of choice for LLMs. - The importance of traversability in API design. - How the expectations for API design have evolved over time. - What defines a great Developer Experience. ### Listen on [Apple Podcasts](https://podcasts.apple.com/us/podcast/request-response/id1801805306?i=1000698928723) | [Spotify](https://open.spotify.com/episode/29i1yj8mVO6PiCl4Hizll3?si=e89864f088e644bb) ## Show Notes [00:00:00] **Introduction** - Sagar welcomes John Kodumal to the podcast. - John's background as co-founder and former CTO of LaunchDarkly. [00:00:28] **What is LaunchDarkly?** - LaunchDarkly's role in separating deployment from release. - The impact of fine-grained feature flag controls. - Benefits: safer rollouts, experimentation, controlled beta releases. [00:02:18] **Pre-LaunchDarkly Era** - How companies previously handled feature releases. - Homegrown tools and their limitations. - Inspiration from early DevOps talks, including Flickr's feature flagging. [00:05:26] **LaunchDarkly's API and SDK Focus** - The importance of embedding into the software development lifecycle. - Learning from Atlassian's approach to API-driven development. - Early investment in API design and OpenAPI specifications. [00:10:37] **Technical Deep Dive: SSE and Feature Flags** - Use of Server-Sent Events (SSE) for real-time feature flagging. - Challenges with existing SSE open-source libraries. - Ensuring consistency across distributed systems. [00:14:23] **The Evolution of API Expectations** - Increased demand for standardized, high-quality APIs. - Importance of pagination, rate limiting, and caching. - Growing role of OpenAPI specifications in modern API development. [00:19:26] **Traversability and Developer Experience** - APIs as traversable maps for both humans and machines. - Comparison to REST principles and the Fielding Thesis. - How great APIs lower integration barriers for developers and AI agents. [00:22:40] **What Defines Great Developer Experience?** - Macro and micro-scale perspectives on DevEx. - Anticipating user needs and providing escape hatches. - Importance of small but impactful UX details. [00:26:23] **Closing Thoughts** - The significance of developer experience in building tools. - How LaunchDarkly has influenced modern software development. ### More John Kodumal Quotes From The Discussion 1. **Feature Flags Are Powerful** "A lot of people look at just that idea of feature flags, and they think that it's small, and they're shocked that there's a multi billion dollar company that does feature flags. But it's not about feature flags. It's about all the changes to the software development lifecycle that feature flags enable" 2. **Traversability Over Endpoints** "Nothing should be an endpoint. Everything should be traversable. If you have an endpoint, it's broken." 3. **Working With The Complexity of Developer Needs** "In order to fit into someone's SDLC you have to respect the differences that make their software development practices unique---you have to work with them, not force a one-size-fits-all solution." ### Referenced - [Flickr DevOps Talk on Feature Flagging 2009](https://www.youtube.com/watch?v=LdOe18KhtT4) - [OpenAPI Specification](https://spec.openapis.org/#specifications) Production by [Shapeshift](https://shapeshift.so). For inquiries about guesting on Request // Response, email [samantha.wen@speakeasy.com](mailto:samantha.wen@speakeasy.com). # Transcript Source: https://speakeasy.com/blog/request-response-ken-rose import { NewsletterSignUp } from "@/components/newsletter-signup"; import { PodcastCard, PodcastPlayer } from "@/components/podcast"; import { kenRoseEpisode } from "@/lib/data/request-response";
[Ken Rose](https://www.linkedin.com/in/klprose/) is the CTO and co-founder of [OpsLevel](https://opslevel.com/). In this episode, Ken shares the founding journey of OpsLevel and lessons from his time at PagerDuty and Shopify. We discuss: - GraphQL vs REST - The API metrics that matter - How LLMs and agentic workflows could reshape developer productivity. ### Listen on [Apple Podcasts](https://podcasts.apple.com/us/podcast/anthropic-mcp-graphql-vs-rest-and-api-strategies-for/id1801805306?i=1000703080125) | [Spotify](https://open.spotify.com/episode/3gDRkAk1rq0Y4f1PFNL51y?si=_aRaEq7cSlGZ1kZBhWLByw)
## **Show Notes:** - [00:01:08] What OpsLevel does and why internal developer portals matter - [00:01:51] Ken's journey from PagerDuty to Shopify and to starting OpsLevel - [00:04:03] Developer empathy, platform engineering, and founding OpsLevel - [00:05:02] OpsLevel's API-first approach and extensibility focus - [00:06:26] Using GraphQL at scale—Shopify's journey and lessons - [00:08:30] Managing GraphQL performance and versioning - [00:10:12] GraphQL vs REST – developer expectations and real-world tradeoffs - [00:11:27] Key metrics to track in API platforms - [00:12:50] Why not every feature should have an API—and how to decide - [00:13:48] Advice for API teams launching today - [00:14:44] API consistency and avoiding Conway's Law - [00:15:50] Agentic APIs, LLMs, and the challenge of semantic understanding - [00:18:48] Why LLM-driven API interaction isn't quite magic—yet - [00:20:00] Internal APIs as the next frontier for LLM productivity - [00:21:43] 5x–10x improvements in DX via LLMs + internal API visibility - [00:23:00] API consolidation, discoverability, and LLM-powered developer tools - [00:23:54] What great developer experience (DevEx) actually looks like ## More Quotes From The Discussion ### Challenges of Running GraphQL > "I can tell you as OpsLevel, running a GraphQL API, you know, all the usual things, making sure that like you don't have n+1 queries, ensuring, you don't get per resource rate limiting like you do with REST... You have to kind of be more intentional about things like query complexity. Those are challenges you end up having to solve. > > Versioning is another big one. We are far enough along in our journey. We haven't done a major version bump yet. I know a few years ago, after I left Shopify, they switched their GraphQL schema to be versioned and now they have much more kind of program management around like every six months they have a new major version bump. > > They require clients to migrate to the latest version, but that's effort and that's calories that you have to spend in that kind of API program management." ### DevEx of REST vs GraphQL > "We have a customer that has been with us for years, but our champion there hates GraphQL. Like he really hates it. And the first time he told me that I actually thought he was like, you know, joking or being sarcastic. > > No. He was legitimate and serious because from his perspective, he has a certain workflow he's trying to accomplish, like a "Guys, I just need a list of services. Why can't I just like REST, even make a REST call and fetch last services and that's it. Why do I have to do all this stuff and build up a query and pass in this particular print?" > > And I get that frustration for developers that, you know, REST is sort of easier to start. It's easier just to create a curl request and be done with it, right? It's easier to pipe, the output of a REST call, which is generally just a nice JSON up into whatever you want versus "No, you've actually have to define the schema and things change." > > I think GraphQL solves a certain set of problems, again, around over fetching and if you have a variety of different clients. But there is this attractive part of REST, which is "I just made a single API call the single endpoint to get the one thing I needed, and that was it." And if your use case is that, then the complexity of GraphQL can really be overwhelming." ### API Consistency and Conway's Law > "I do think consistency is an important thing, especially when you're dealing with a large company that has disparate parts working on different aspects of the API. You don't want Conway's Law to appear in your API—you know, where you can see the organizational structure reflected in how the API is shipped. > > So making sure that an API platform team or someone is providing guidance on how your organization thinks about the shape of APIs is crucial. Here's how you should structure requests. Here's how you should name parameters. Here's what response formats should look like. Here's consistent context for returns and responses. > > Here's how to implement pagination. It's all about ensuring consistency because the most frustrating thing as an API client is when you hit one endpoint and it works a certain way, then you hit another endpoint and wonder, 'Why is this structured differently? Why does this feel different?' That can be tremendously difficult. So having some guardrails or mechanisms to ensure consistency across an API surface area is really valuable." ## Referenced - [OpsLevel](https://opslevel.com/) - [Shopify Storefront API](https://shopify.dev/docs/api/storefront) - [GraphQL](https://graphql.org/) - [REST](https://web.archive.org/web/20250908222822/http://restfulapi.net/) - [Conway's Law](https://en.wikipedia.org/wiki/Conway%27s_law) - [RED Metrics](https://grafana.com/blog/2018/08/02/the-red-method-how-to-instrument-your-services/) - [Anthropic MCP](https://www.anthropic.com/news/model-context-protocol) - [Agents.js](https://huggingface.co/blog/agents-js) Production by [Shapeshift](https://shapeshift.so). For inquiries about guesting on Request // Response, email [samantha.wen@speakeasy.com](mailto:samantha.wen@speakeasy.com). ## Introduction [00:00:05] **Nolan**: Hey guys, this is Nolan. I do developer relations at Speakeasy. Thanks so much for taking the time to listen to the podcast. This is a new format for us and we're actively looking to get feedback from the community. Would love if you guys could drop us an email, you can email me, nolan@speakeasy.com, or our producer Ira, ira@speakeasy.com. [00:00:23] If you're enjoying the content, don't forget to sign up below, drop us your email, and you'll be the first to hear about new guests that are coming on. Get episodes straight to your inbox as soon as they release. Thanks so much for listening and enjoy the rest of the episode. ## Welcome and OpsLevel Introduction [00:00:34] **Sagar Batchu**: Hey everyone. Welcome to another episode of Request and Response. I'm your host, Sagar, co-founder, CEO of Speakeasy. I'm joined today with Ken Rose, CTO, and co-founder of OpsLevel. Ken, how are you doing today? [00:00:47] **Ken Rose**: I am doing great. Thank you. Thanks for having me on the show. [00:00:49] **Sagar Batchu**: Absolutely. I hear you're a pro at doing podcasts, so really I think we should swap places here. [00:00:55] **Ken Rose**: It's definitely new for me to be in the guest seat. It's been a while. Normally I'm the host, but yeah, it's fun to be on this side of the aisle today. [00:01:01] **Sagar Batchu**: That's awesome. Yeah. Cool. To kick things off, I would love for you to share a bit more about OpsLevel and what you guys do. [00:01:08] **Ken Rose**: Yeah, definitely. So at OpsLevel, we make a product called an internal developer portal. And really this is a tool that helps engineering teams manage the complexity of really large software environments. And we help consolidate a bunch of the context from various tools that sort of exist out there across the building of software, the deployment of software, the management of software and production into a single, consolidated place so that engineering teams can ship more reliably and more securely. ## The Journey to OpsLevel [00:01:35] **Sagar Batchu**: That's amazing. It honestly sounds like a product I know a lot of our customers would be interested in. I've worked at a few companies where building out like this single pane of glass is such a tough thing to do at scale. So it sounds like you're solving a real problem. How did you come to working on this problem? What's your journey to OpsLevel? [00:01:51] **Ken Rose**: Yeah, it actually begins, oh man, like more than 12 years ago now. My co-founder, John, was the first employee at PagerDuty, right? The kind of, you know, big incident management company. And I was considering being their first Canadian employee. I was, I'm based here in Toronto, and at the time, again, it's 12 years ago now. It was like, actually I wanna go start a startup. I really wanted to start my own company and do my own thing. And I tried something in like real estate and it didn't really go anywhere. And then I was like, you know, kind of went back from my tail between my legs "yeah, John, is that job offer still there?" [00:02:19] I thought, you know, go back to PagerDuty. I worked for a year and then try again. Oh, that one year turned to four and John and I became really, you know, good friends and yeah, kind of like that seed was always there. And so I ended up going to another company after PagerDuty. It's a small e-commerce company here in Canada called Shopify. [00:02:35] And then I actually was on a parental leave. My daughter was born at the time and I kind of had that space to think about, what do I want in life? You know, what's next? And that entrepreneurship bug was kind of still there. And so John also had a similar kind of fire brewing in his belly, and he reached out and we started, you know, getting to talking. [00:02:52] And we had the, you know, the kind of list of all the possible startup ideas where a big list of 50 things, but whittled it down to two. And so one of them was a better applicant tracking system. So John and I had both done as engineering leaders, just a lot of interviews with engineering candidates. [00:03:05] I won't name which tool we use, but it wasn't very good. It's still around, so I don't wanna name them. But we're like, okay, we can build something better here. But in doing a lot of discovery, we realized the buyer for that is gonna be HR people. And I love my HR VP, but I wasn't sure if I wanted to commit 10 years to working with that person or that persona. [00:03:21] And the other idea was something in and around OpsLevel, and we had seen this idea of cataloging your services, building internal tooling to help development teams move more efficiently. We had seen that problem being solved internally as an internal tool at just a variety of organizations. We talked to Spotify before they'd open source Backstage, Shopify had their own version, LinkedIn, Microsoft, Groupon, like all these companies had basically reinvented the wheel, solving the same problem. [00:03:45] And the thing we also love was the personas just it's other developers, it's other folks that were like John and myself. It's developers, it's platform engineering leaders. It's engineering leadership at large. That's my jam. I just, I love those folks, right? I, that's the area I live in. The amount of empathy I have for that, the set of problems that those groups have is huge. [00:04:03] So that was just, it was a very natural pull. And you know, we're really fortunate years later, our, you know, company's still thriving, growing, adding on new customers, and just really lucky to be able to kind of found lightning in a bottle and being able to build this great product for all of our customers. ## APIs and OpsLevel [00:04:16] **Sagar Batchu**: That's amazing. I can resonate with so much of that story to be honest, that platform engineering is something I nerd out on so much. It honestly is that core part of many tech businesses today that is kind of silent hero in the background, giving the rest of the company velocity and the ability to create amazing products. [00:04:35] The internal API and just more overall problem that OpsLevel is solving is not too far from actually why we started Speakeasy and wanting to kind of go after and bring consistency and governance and really, you know, just sanity to API development. I honestly feel like OpsLevel is, yeah would be something I could have used at any of the past companies I worked at, so that's really amazing. [00:04:57] Does OpsLevel have an API or how do APIs kind of fit into the OpsLevel story? [00:05:02] **Ken Rose**: Yeah, no we have a large API, in fact so before starting OpsLevel years ago, again, when I was at PagerDuty, I was responsible, I as the sort of technical lead for their API. And then when I moved over to Shopify, I was the lead for their GraphQL API. Shopify has, as you know, massive API. Also, they had this API patterns team that was responsible for shepherding a lot of the shape of the API and was responsible for those kind of cross-cutting API concerns. [00:05:25] For our API, we are very much like an API driven company. If you think about what goes into an internal developer portal, there's an aspect of a software catalog. There is aspects of standardization, there's aspects of developer self-service. There's a ton of automation that has to go into driving each of those pillars. [00:05:44] And the only way to achieve that automation is through an API. So pretty much everything we build as part of our platform strategy, we need to be API first because we really wanna be able to solve a large percentage of our customer's problems, you know, as a product, but there's always gonna be some long tail that we won't be able to solve directly, right? [00:06:01] Our scrappy team of product dev folks. And so for that, we need our APIs to be allowing for extensibility allowing for customization so that our customers can self-serve themselves. So, if there's some tool we don't support, if there is some particular workflow that like, makes sense maybe for one customer, but not for our base at large, we wanna make sure that our APIs are present and there to be able to do that. [00:06:20] So that means that pretty much everything, and it's a very broad product suite that you can do through the UI. There has to be an API for as well. ## GraphQL vs REST: The Shopify Experience [00:06:26] **Sagar Batchu**: That's fantastic. Going back to really cool that you worked at Shopify during, I think such an amazing time there. One thing that stands out to me as an API developer, Shopify always got a lot of attention for being a company that actually used the GraphQL API externally with their customers. [00:06:42] And I thought it was really cool to see that hot new technology actually go to scale and be used publicly. I feel like since then maybe this is a hot take, but since then, kind of the popularity on GraphQL, especially for external use cases, has gone up and down. There's been a lot of debate around whether, you know, REST or GraphQL is the ultimate option. [00:07:02] I know there's no true answer, but I'm curious if that ever came up at Shopify. Did your customers, were they ever surprised that they had to integrate with GraphQL? [00:07:10] **Ken Rose**: So I mean, I can talk a little bit and, you know, there's history there, right? So I'm trying to remember this, like 2017, 2018, right? So I believe there was the marketplace API, and then there was the admin API. So I actually launched the admin API when I was there, right? It was Shopify has their marketplace API which was it marketplace or storefront? It might've been storefront, but it was like a smaller subset, APIs. [00:07:31] And that was almost like the test bed for GraphQL at Shopify. And that proved valuable for, you know, things like all the usual benefits that GraphQL provides, right? Because GraphQL kind of came out in 2015 from Facebook. But you know, ensuring that there's no over fetching, ensuring better client experience. [00:07:42] And so then there was this decision that they should migrate to their admin API, which is like the commerce API onto GraphQL. And the reasoning at a very high level is look, commerce is a graph. We had a very similar thesis when we started OpsLevel. We have a GraphQL API internally, because software development is a graph. [00:07:59] There's teams, there's microservices, there's applications, there's this tapestry of stuff that's kind of all connected, right? So GraphQL kind of makes sense there. Back to whether or not developers were surprised, I mean I don't think anyone was surprised. 'cause you know, there's kind of enough lead up to the fact like, look, there was already this one API that was, you know, sort of using GraphQL. [00:08:17] There was a lot of support. Shopify maintained backwards compatibility with their existing REST APIs for, you know, some period of time. So there was an overlap period to allow for that transition. Yeah, so, you know, I think overall it was like for Shopify and net positive, but it wasn't without a lot of the usual hiccups that people kind of encounter when they're dealing with GraphQL, right? Both as a client and as a company that provides it. ## GraphQL Implementation Challenges [00:08:30] On our side, I can tell you as OpsLevel, running a GraphQL API, you know, all the usual things, making sure that like you don't have N plus one queries, ensuring, you know, you don't get per resource rate limiting like you do with REST, you have to kind of be more intentional about things like query complexity. Those are, you know, challenges you have to end up having to solve. Versioning is another big one. You know, we are far enough along in our journey. We haven't done a major version bump yet. [00:08:58] I know a few years ago, this was after I left Shopify based switched their GraphQL schema to be versioned and now they have much more kind of program management around like every, I think it's every six months they have a new major version bump. They require clients to migrate to the latest version, but that's effort and that's calories that you have to spend in that kind of API program management. [00:09:15] **Sagar Batchu**: Yeah, that makes sense. I think that's really where the argument kind of goes astray. A lot of the times I think is people you know, compare GraphQL to REST and then immediately look at how they're used externally, but they don't realize the kind of practices that go behind running these two different kinds of APIs is vastly different. [00:09:33] And I think you raised a really good point, like commerce is a graph and that shows like kind of your choice on GraphQL was rooted in a use case that made sense. And I think it always reminds us as developers like not to debate these things in the abstract. That is kind of a key use case behind why you might choose GraphQL, and I think Shopify just happened to be one of the first ones at scale with a very, very clear use case for it. [00:09:57] **Ken Rose**: Yeah, it's funny. I agree with everything you just said and nevertheless, we have, you know, some customers, we deal with developers, right? And sometimes developers are prickly is, is maybe a way I would say describe it. So I know there's one customer I think about in particular, I love them. They're, a fantastic customer. [00:10:12] They've been a customer of us for years, but our champion there is he hates GraphQL. Like he really hates it. And the first time he told me that I actually thought he was like, you know, joking or being sarcastic. No he was legitimate and serious because from his perspective, like he, you know, he has a certain workflow he's trying to accomplish, like a "Guys, I just need a list of services. Why can't I just like REST, even make a REST call and fetch last services and that's it. Why do I have to do all this stuff and build up a query and pass in this particular print?" And I get that frustration for developers that, you know, REST is sort of easier to start. It's easier just to create a curl request and be done with it, right? It's easier to pipe, the output of a REST call, which is generally just a nice JSON up into whatever you want versus "No, you've actually have to define the schema and things change." I think GraphQL solves a certain set of problems, again, around over fetching and if you have a variety of different clients. But for, you know, there is this attractive part of REST, which is "I just made a single API call the single endpoint to get the one thing I needed, and that was it." And if your use case is that the complexity of GraphQL can really be overwhelming. ## Measuring API Success [00:11:05] **Sagar Batchu**: Totally. Yeah. There's like an enhanced simplicity in just exposing your API one resource per, you know, business object or business use case that you want your customers to take advantage of. Yeah I totally resonate with that. You know, just on that API point again, what were some of the metrics you all looked at when you were running these APIs at scale, at Shopify, or even now at OpsLevel? [00:11:27] What metrics to you, do you think that an API team should measure? [00:11:32] **Ken Rose**: Yeah. I mean, I probably would be more apt to talk about what we do at OpsLevel. 'cause that's just a lot more recent. I don't know what Shopify looks at, there's two classes, right? There's kinda just the operational metrics, like things like the RED metrics, right? [00:11:44] Request errors, duration just understand like how is your API performing? For us it's looking at specific, you know, queries that are coming in and just understand like which ones are, you know, potentially non-performing or what are some areas we have to fix. We have things like, you know, N plus one detection to make sure that like we don't, you know, explode. [00:12:00] The other kind of like qualitative one has been feature requests that we get around missing capability in our API. Like I did just finish saying earlier, you know, part of our platform strategy is this sort of Pareto principle. Like we wanna make sure that for problems that are, you know, in the box, like something that 80% of our customer base wants, that's something we should solve as a first class thing. [00:12:20] We obviously will have API for it. But we always wanna make sure those APIs exist. 'cause if we don't solve for something, you know, there's an API to do it. That said, sometimes there are things that we will intentionally not have an API for. There's sometimes that's due to performance or sometimes it's, that's due to the UX is really meant to be more human centered. [00:12:35] It's not really meant for like the machine to wanna ingest data or push data to do something with, but we do look at if we're getting requests for that, how does that translate to the fidelity then of what we're wanting to do with the API? It becomes sort of that backstop to justify yes, this decision we made to not put this in API maybe that was wrong. [00:12:50] Or maybe our assumptions about like why we did that were actually correct. Because now we're getting some input that like, oh, here's some new use case where they do wanna use API for this particular thing. [00:12:57] **Sagar Batchu**: I love that framing because I see a lot of times with companies, they kind of expose a very large API, but then most of the traffic will be on like two endpoints, so one endpoint, and so they don't really have an understanding of which parts of the API actually getting used. And so they've actually exposed a huge part of the API and created a massive dependency for their customers. [00:13:18] And they move a lot slower as result because they have to update, you know, a hundred endpoints instead of just two, which is like where all the value is. So I love the kind of framing on metrics that you guys have. If, you know, I'm curious if someone is launching an API today and you had to give them like one or two top things to look for. [00:13:36] What would you advise them on? This is something I see. It's so funny, I see this on LinkedIn all the time where there's like API product managers talking about this. They'll be like the top two or three metrics that an API team should look at. I don't know if you have a point of view on that. ## Key Advice for API Launches [00:13:48] **Ken Rose**: Oh man. I mean, I think it depends a lot on the business and like the scale that API is gonna be hitting. Like I could talk about things in general. Like again, think about like your RED metrics, think about versioning of your API and how you're gonna manage deprecations. It's almost I like the term API product manager. [00:14:02] It's put on your product management hat. What is the value of this API's providing? Why are your users using it? What are they trying to do? What problem are they trying to solve and how's your API help 'em accomplish that? Then just focus on that repeatedly and make it, you know, refine it until it's as simple as it could possibly be. [00:14:15] I think that would be like the ultimate lesson. Everything else after that is just details in the pursuit of that goal. [00:14:20] **Sagar Batchu**: Yeah, that makes a lot of sense. I think in some ways API platform teams sometimes have to wear that product manager hat for the API. Especially at at larger companies, you have each team ships a part of the API and gets packaged up as like a single library or a single documentation site. [00:14:37] Unless you have that API platform team owning those metrics and wearing the product manager hat, you often don't get that thinking. So, yeah, that makes a lot of sense. ## API Consistency Across Organizations [00:14:44] **Ken Rose**: There is actually one now that you mentioned the API platform team. I do think consistency is an important thing, especially when you are dealing, if you have a large company, you have disparate parts of the company working on different parts of the API. You don't want to have, is it Conway's Law that you can kind of, you know, you see the shipping of the, you don't want Conway's Law to appear in your API. [00:15:01] So making sure that API platform team or someone is providing guidance on like here's how our organization thinks about the shape of APIs. Here's how you should think about structuring requests. Here's how you should name parameters. Here's what response format should look like. Here's consistent context of return and responses. [00:15:14] Here's how to do a pagination. But it just ensuring that consistency because the most frustrating thing as a an API client is okay, I hit this REST, hit this one end point. It works this way. Hit this other point. Why is this structured differently? Like, why does this feel different? Yeah, that can be tremendously difficult. [00:15:27] So just, you know, having some guardrails or mechanisms to ensure consistency across an API surface area. Also really valuable. [00:15:34] **Sagar Batchu**: Yeah, absolutely. I think I'm so glad you brought up, it's like the number one thing that I see when I look at like an API, that's old, like a legacy spec, and since we deal with specs I look to the specs and I see I think I can literally draw the org structure by looking at that schema document and being like, "Hey these two are designed by one team, these two are designed by another team. And then this one was tacked on later, right?" Like this such a clear history evolution built into it. No I love that analogy. ## Frequently Asked Questions ### What is OpsLevel? OpsLevel is an internal developer portal that helps engineering teams manage the complexity of large software environments. # request-response-robert-ross Source: https://speakeasy.com/blog/request-response-robert-ross import { NewsletterSignUp, PodcastPlayer, YouTube } from "@/lib/mdx/components"; On the second episode of Request // Response, I speak with [Robert Ross](https://www.linkedin.com/in/bobby-tables/), CEO of [FireHydrant](https://firehydrant.com/). We discussed his journey building FireHydrant, the evolution of API design, and the impact of gRPC and REST on developer experience. We also talked about the role of LLMs in API design, the shift towards data consumption trends in enterprises, and how great developer experience is measured by a simple litmus test. ### Listen on [Apple Podcasts](https://podcasts.apple.com/us/podcast/request-response/id1801805306?i=1000698928724) | [Spotify](https://open.spotify.com/episode/0OZRnw1dUfFydxQt9tnTqg?si=llkwy5XBRMWkPtidMOfVJQ) ## **Show Notes:** [00:00:00] **Introduction** - Overview of discussion topics: building FireHydrant, gRPC, API design trends, and LLMs in APIs [00:00:42] **The Story Behind FireHydrant** - Robert's background in on-call engineering and why he built FireHydrant - The problem of incidents and automation gaps in on-call engineering [00:02:16] **APIs at FireHydrant** - FireHydrant's API-first architecture from day one - Moving away from Rails controllers to a JavaScript frontend with API calls - Today, over 350 public API endpoints power FireHydrant's frontend and customer integrations [00:03:50] **Why gRPC?** - Initial adoption of gRPC for contract-based APIs - Evolution to a REST-based JSON API but with lessons from protocol buffers - Would Robert choose gRPC today? His thoughts on API design best practices [00:06:40] **Design-First API Development** - The advantages of Protocol Buffers over OpenAPI for API design - How API-first development improves collaboration and review - Challenges with OpenAPI's verbosity vs. Protobuf's simplicity [00:08:23] **Enterprise API Consumption Trends** - Shift from data-push models to API-first data pulls - Companies scraping API every 5 minutes vs. traditional data lake ingestion - FireHydrant's most popular API endpoint: Get Incidents API [00:10:11] **Evolving Data Exposure in APIs** - Considering webhooks and real-time data streams for API consumers - Internal FireHydrant Pub/Sub architecture - Future vision: "Firehose for FireHydrant" (real-time streaming API) [00:12:02] **Measuring API Success (KPIs & Metrics)** - Time to first byte (TTFB) as a key metric - API reliability: retry rates, latency tracking, and avoiding thundering herd issues - The challenge of maintaining six years of stable API structure [00:17:12] **API Ergonomics & Developer Experience** - Why Jira's API is one of the worst to integrate with - The importance of API usability for long-term adoption - Companies with great API design (e.g., Linear) [00:18:14] **LLMs and API Design** - How LLMs help in API design validation - Are LLMs changing API consumption patterns? - Rethinking API naming conventions for AI agents [00:22:02] **Future of API Ergonomics for AI & Agents** - Will there be separate APIs for humans vs. AI agents? - How agentic systems influence API structures - The need for context-rich naming conventions in APIs [00:24:02] **What Defines Great Developer Experience?** - The true test of developer experience: Can you be productive on an airplane? - Importance of great error messages and intuitive API design - The shift towards zero-docs, self-explanatory APIs ### More Robert Ross Quotes From The Discussion 1. **Enterprise Data Teams are Now More Receptive to APIs** "I think more and more people are willing to pull data from your API. There used to be kind of a requirement many years ago of like, you need to push data to me, into my data lake. And the problem with that is, sure, we can push data all day long, but it may not be in the format you want. It may not be frequent enough. Like, you might have stale data if we're doing, a nightly push to our customers' data lakes." 2. **On LLMs and API Design** "I don't think that LLMs are going to fundamentally change how we interact with APIs. At least not in the short term. I think that they'll help us build better clients and I think they'll help us build better APIs, but the moment we start using them, I mean, that's just going to be code calling code." 3. **The Airplane Test for Developer Experience** "When it comes to developer experience, I think that my guiding principle is like, is it easy? Like, how far can I get before I have to read documentation? And I think that includes like great error messages, like, if I can do something on an airplane with really crappy wifi and like be productive, that's great developer experience." ### Referenced - [FireHydrant | The end-to-end incident management platform](https://firehydrant.com/) - [Linear API docs](https://linear.app/docs/api-and-webhooks) - [Offset's Materialize RBAC system](https://materialize.com/docs/manage/access-control/rbac/) - [Speakeasy's Terraform generator](https://www.speakeasy.com/docs/create-terraform) Production by [Shapeshift](https://shapeshift.so). For inquiries about guesting on Request // Response, email [samantha.wen@speakeasy.com](mailto:samantha.wen@speakeasy.com). # Transcript Source: https://speakeasy.com/blog/request-response-sinan-eren import { NewsletterSignUp } from "@/components/newsletter-signup"; import { PodcastCard, PodcastPlayer } from "@/components/podcast"; import { sinanErenEpisode } from "@/lib/data/request-response";
[Sinan Eren](https://www.linkedin.com/in/sinaneren/) is the CEO and co-founder of [Opnova](https://opnova.ai/). In this episode, Sinan shares how Opnova is building AI-powered automation for enterprises, specifically targeting companies that rely on legacy systems and lack modern APIs. We discuss: - Automation in systems without APIs - RPA, LLMs, and new standards like MCP - The rise of browser-based automation - The long-term vision of API-first ecosystems ### Listen on [Apple Podcasts](https://podcasts.apple.com/us/podcast/multimodal-automation-api-democratization-and/id1801805306?i=1000705415680) | [Spotify](https://open.spotify.com/episode/4KaetUSRYsCPRlqmdsZU8r)
## Show Notes ### Introduction [00:00:00] Overview of discussion topics: automation in legacy systems, RPA, LLMs, MCP, and the evolution toward API-first ecosystems ### What OpNova Is Solving [00:00:42] - Sinan's background in cybersecurity and founding OpNova - Automating rework: repetitive, manual tasks in regulated industries like healthcare and finance ### Bridging the API Gap [00:02:00] - Most enterprise systems lack modern APIs - How OpNova helps these companies automate despite missing APIs - The challenges of working beyond the Silicon Valley tech bubble ### Leveraging RPA and LLMs for Automation [00:03:00] - How OpNova uses robotic process automation and large language models - Modeling user behavior to automate UI actions via screenshots and intent recognition ### Standards Like MCP and Tool Calling [00:04:58] - MCP (Model Context Protocol) and its potential to become a new standard - Bridging the gap for underserved industries lacking API exposure - Sinan's take on early adoption and long-tail enterprise needs ### APIs as a Deal-Maker [00:06:00] - APIs enabling last-minute customer wins in previous startups - Command-line over UI: why customers sometimes prefer APIs to interfaces - Rapid feature delivery via API access ### The API Tax Debate [00:07:54] - Comparing API access to the "SSO tax" of the past - Concerns about hiding APIs behind enterprise pricing tiers - Why APIs should be a baseline offering ### APIs as the New Sitemaps [00:09:00] - API discoverability as a critical factor in tool ecosystems - Drawing parallels between SEO-era sitemaps and today's OpenAPI specs - The risk of exclusion from LLM-powered interfaces ### Browser Automation as a Transitional Layer [00:11:58] - Why browser-based agents are a temporary solution - The long-term goal: native APIs everywhere - Transitional tooling as a necessary bridge ### Tool Discovery and Registries [00:13:58] - The need for robust API registries to support tool discovery - From proof-of-concept to production: bridging the enterprise automation gap - The challenge of finding the right tools at the right time ### Closing Thoughts and Opnova's Vision [00:15:28] - Browser orchestration vs. API-driven workflows - Why APIs are the true endgame ## More Quotes From The Discussion ### The API Tax Revolution > "We have this curse in our space, it's changing now—it's called SSO tax, single sign-on tax. Why? You will have like the cheap tier, free tier, and then you'll have the enterprise tier, if you want single sign-on, if you want to tie your Okta, your EntraID into the SaaS, right? You need to buy the enterprise tier so that you can have the benefit of SSO. But I'm noticing APIs are now put in place of SSO. So SSO tax—now I worry that it's becoming API tax." ### APIs as the New Sitemaps for the Agentic Era > "I feel like not having API actually is similar now. It's going to exclude you from a rapidly emerging ecosystem of tools and tool use where the discovery problem is now distributed. And so, like, what sitemaps did for websites was it said, 'Hey, I'm a website. I'm going to broadcast. I do this thing.' And then therefore any scraper ecosystem could pick it up. I think the same thing's happening with APIs." ### Browser Automation: The Necessary Bridge to an API-First Future > "All this browser use models, right? Like computer use, browser use models. They are an intermediary kind of a solution, a transitional solution, right? Because we're waiting for the APIs to be exposed. So nobody really genuinely loves the idea of an agent orchestrating a Chrome browser. Really, it's just a temporary point in time that we have to do it because like you said, like, sitemaps had to be invented for better SEO." ## Referenced - [Anthropic MCP](https://www.anthropic.com/news/model-context-protocol) - [OpNova.ai](https://opnova.ai/) - [Okta](https://www.okta.com/) Production by [Shapeshift](https://shapeshift.so). For inquiries about guesting on Request // Response, email [samantha.wen@speakeasy.com](mailto:samantha.wen@speakeasy.com). ## Introduction [00:00:00] **Sagar Batchu**: Everyone, this is Sagar, CEO of Speakeasy. On today's episode of Request Response, we had Sinan Aron, CEO and co-founder of OpNova, and previously VP at Barracuda Networks. We chatted about the emerging ecosystem of tool use and tool discovery, and how that's helping companies without APIs solve the last mile problem of automation. [00:00:19] **Sagar Batchu**: MCP and tools are super new, but tune in to learn how big enterprises are leveraging them today. [00:00:24] **Sagar Batchu**: Everyone. Welcome to another episode of Request Response. I'm your host, Sagar, co-founder of Speakeasy. We are the modern tool chain for API development. I'm joined today by Sinan Aron, co-founder and CEO of OpNova and previously VP at Barracuda Networks. Sinan, how are you doing today? [00:00:42] **Sinan**: Good. Great. Good to be here. Thanks. ## About OpNova: Automating without APIs [00:00:44] **Sagar Batchu**: You told me a lot of great things we want to talk about today, but I'd love to start by just learning more about OpNova, what you guys do, kind of your background as well. [00:00:52] **Sinan**: Yeah, sure. Happy to. Personal background. I've been in cyber security for over 20 plus years now. But mainly in cyber security use cases at a starting point, but we are able to automate entire back office functions for regulated industries. [00:01:06] **Sinan**: We started with a focus on repetitive, mundane and error-prone tasks, which we call rework - repetitive work. [00:01:12] **Sagar Batchu**: That's really cool. I feel like APIs for you are such a unique thing, but like for most companies, we think about APIs as just, hey, you either have a public API, that's how you know, you ship your product, it's your revenue driver, or you have internal APIs and how you build. I feel like you are really working with companies and ecosystems that almost don't have them, and you're kind of bringing some of the automation that usually isn't possible when those pre-existing APIs don't exist. [00:01:38] **Sinan**: That's spot on. That's what I was excited about this conversation because, you know, look. If we have APIs, we will use APIs, right? That will be the preferred mode of execution automation, right? That would make things a lot more deterministic, a lot simpler, a lot more efficient, you know, cost effective, you name it. [00:01:56] **Sinan**: Therefore, you know, these internal applications or some of these legacy applications with potential overlays, if they can expose APIs, will do our jobs much better. But you're right, we're kind of trying to figure it out and bridge that gap of lack of APIs, lack of modern protocols. [00:02:11] **Sinan**: How can we expand these platforms and connect it to them? ## The API Gap in Non-Tech Industries [00:02:16] **Sagar Batchu**: New age suddenly where I think we're realizing with, you know, with LLMs, with AI, there's so much automation, operational work we can offload, but so many systems today actually don't have APIs. I was reading some crazy stats the other day around how just so many websites and products out there don't have APIs. [00:02:37] **Sagar Batchu**: And I think in the, in maybe the, you know, Silicon Valley and broader VC power tech ecosystem where we somewhat take for granted that every system out there has any programmatic interface to leverage, but really, the majority of them don't. And you look across, you know, Healthcare insurance, as you said, like, there's so many industries where APIs are not the norm. [00:02:58] **Sinan**: You definitely tapped on something important. We do not sell or work with, you know, these early design partners anybody in the valley, right? We want it to expand beyond this tech bubble. This time we're looking into completely outside of the valley, trying to serve the underserved. They do want to be more modern and cost effective, but yet some of these applications that they depend on a daily basis do not offer any means for automation or improvement. So that's where we come in and try to help them out. ## Bringing Automation to Legacy Systems [00:03:26] **Sagar Batchu**: Yeah, that's super cool. I'd love to understand a little bit more how, you know, you're bringing that kind of automation when systems don't have APIs, like kind of what tech are you leveraging? [00:03:37] **Sinan**: That's right. I'm sure a lot of the folks are familiar with kind of the RPA, Robotic Process Automation. [00:03:44] **Sinan**: So think about all these amazing multimodality models that we can leverage to do a recording, have that recording break into chunks, understand the intent behind the action and from that built an automation prompt or a template, then the agent can repeat you know, take a screenshot. [00:04:03] **Sinan**: You know, process the screenshot, understand the intent for the action, decide what the next action should look like, then translate that action into a click, a text entry, a drag and drop it, you know, a scroll. So that's kind of how it works. It models essentially the operator doing the task from that. It generates some byproducts that you can use to automate it end to end. ## The Evolution of Tool Calling and MCP [00:04:25] **Sagar Batchu**: You brought on an interesting point and, you know, our own tool calling and the ecosystem suddenly has seen like, literally in the last few weeks, like a real surge and excitement on, like, what tools can unlock for companies and both internally externally. I think a lot of it is actually centered around kind of automation experience that you've described. We've seen a ton of movement here in the kind of overall tool calling landscape. I think one that stands out is MCP from Anthropic, the Model Context Protocol. How do you see this evolving? Like, do you think something like MCP is going to become the new standard? [00:04:59] **Sinan**: It might very well become the lay of the land and the standard here in the valley. Or maybe in the East Coast in New York, you know, the tech centers of the country. I can see that. Right? I'd become the standard for builders. Like, if you're building a SAS product, a vertical SAS product, whatever it is, you're going to definitely standardize behind one of these, you know, protocols. [00:05:19] **Sinan**: I get it. But what I see with the long tail of, you know, all these companies that I mentioned, the underserved, right? What I see is that there's going to be a lot of bridging. [00:05:30] **Sinan**: I can totally see in this long tail of underserved industries that could be MCP bridges. That's how usually these things are adapted by the tech industry very quickly. You know that one of them becomes the dominant play. Great. We are all happy, you know, with each other, but when it comes to these, you know, manufacturing, banking, credit unions, right, they can benefit maybe with some sort of a bridge, a proxy approach to those modernized protocol. ## The Future of API Consumption [00:05:58] **Sagar Batchu**: I'm also kind of curious to get your take. What changes are you anticipating in how API consumption is going to change with this new model, right? Do you foresee like a broader adoption? [00:06:10] **Sinan**: Yeah, I'll share kind of, anecdote from the former startups that I build, or I work that I see, like, tremendous value in APIs because we were able to, you know, for example, ship a feature. Let's say a deal depends on a particular feature, right? There's always that, like the customer is going to ask you for something that you thought about, but you didn't prioritize. [00:06:30] **Sinan**: And now, in order to win the deal, you have to ship it, right? It could be some security feature. It could be some sort of an integration. What I found out, you know, that we were able to say, you know what, the UX, the UI part is going to take time. But why don't I ship you an API right, and then tie it to our CLI, and then you get to consume it that way. [00:06:50] **Sinan**: Guess what? In almost all cases, we won that deal, and hardly ever baked it into the UI, right, because it was good enough. It was actually, in some cases, preferred, because people have this fatigue about going between, you know, management interfaces of 100 different tools, especially if you're in IT, especially you're in cyber security, right? They'd rather have CLIs that they can bake into some automation that can just orchestrate API. They don't really need to see your crappy UI really. So API saved me so many times because if I went through the design process that would have pushed us out three months at a minimum, and we would have lost that deal, right? [00:07:30] **Sinan**: But I do have a question in return for you. ## The Emerging "API Tax" Concern [00:07:32] **Sinan**: Actually, the only concern that I have is you notice that I've been talking a lot about identity. I am, you know, a lot of cyber security around user management. So we have this curse in our space, it's changing now, it's called SSO tax, single sign on tax. Why? You will have like the cheap tier, free tier, and then you will have the enterprise tier, if you want single sign on, if you want to tie your Okta, your EntriD into the SaaS, right? [00:07:59] **Sinan**: You need to buy the enterprise tier so that you can have the benefit of SSO, the benefit of two factor. This was usually You know, there's like SSO wall of shame. It's been, you know, vendors who do this practice have been shamed for the last decade or so, right? Now they're changing their tune. SSO is almost by default. [00:08:17] **Sinan**: It's not hidden behind some sort of a very steep enterprise pricing tier. But I'm noticing APIs are now put in place of SSO. So SSO tax, now I worry that it's becoming API tax. So what do you think? Do you observe this? Where do you think this is going? [00:08:33] **Sagar Batchu**: Yes. Wow. I love this. Such an interesting point of view. I think like over the last 10 years, like we've seen a couple of shifts in the developer ecosystem and in SAS in general. Like, I think there was a time and no SAS had APIs and like it was very much exception. And then you've had this kind of API first mark both modernization as well as a business, you know, kind of business model update that a lot of sass went through the last 15, 20 years. And then now, I think we've gone to the point where I actually feel that, like, for the best companies out there, not going API first is a is actually a huge mistake and a mess in terms of being able to do early signal and discovery around how people would use their product. Of course, there's always exceptions. There's some businesses where, like, your ICP just has no, you know, need to integrate with an API. That being said, I think now, all of a sudden, with tool calling, like, if you have an API, sure, your ICP may not be developers, but someone may make an MCP server for your API and then let the non-developer persona actually access the data in the API through, you know, whatever MCP client, whether it's Cloud or Cursor or whatever. [00:09:43] **Sagar Batchu**: So I think actually that it's a mistake to have APIs behind kind of enterprise tier of walled garden because API is no longer just the interface for developer persona. It's actually interface now for all personas. [00:09:58] **Sinan**: That makes a lot of sense. So in the fullness of time, you think that this API tax is going to disappear because the need is immense. It's not just a luxury to offer it up to developers, but it's just, you know, part of the tool use. It's just, if you expose any kind of LLM driven workloads. If you want to expose it to the Chet GPTs of the world, it needs to be available right through an MCP set up. Yeah that, that's hopeful. I appreciate that. Yeah, that's a vision because some of these companies, yeah, they might afford it, but they really turned off by this, right? Like it's feels like it's just a basic block like SSO that should come by default, not as an add on. ## APIs as the New Sitemaps [00:10:36] **Sagar Batchu**: Yeah, I do agree. I think it's no longer an add on. Another kind of analogy I've been thinking about and just to get your take is, you know, a lot of websites went through an evolution in the last, you know, early in the century around making sure they have site maps so that web crawlers can go out and kind of get information on them. [00:10:53] **Sagar Batchu**: It works with SEO. I feel like not having API actually is similar now. Like, it's going to exclude you from a rapidly emerging ecosystem of tools and tool use where the discovery problem is now distributed. And so, like, the, what sitemaps did for websites was it said, Hey, I'm a website, broadcast. I do this thing. And then therefore any scraper SEO system could pick it up. I think same things happening with APIs, right? Like people try to build centralized catalogs and governance platforms and all of that. And we're realizing like, adoption of that is really hard. Instead, if everyone launches and releases an API, and then like, let's say a tool definition, then suddenly there's a, you know, a wide variety of clients that can take advantage of you. So, yeah, that's the analogy I'm seeing is like not having, you know, a public OpenAPI spec and not having an API broadcasted means you're just going to get excluded from this kind of LLM and agentic ecosystem. ## Implications for B2C and Commerce [00:11:49] **Sinan**: That makes a ton of sense. I'm thinking in my day to day use of these, you know, these models like perplexity search is now almost, you know, 100 percent of the time I would say baked into that chat like interface, right? I don't navigate to web pages. I kind of get something dense. Maybe I can follow some reference, some link to the website if I want to get to more details or whatever. [00:12:12] **Sinan**: But so if that's the experience, if the e-commerce experience is going to become that, if you are a B2C business, I guess it might be super damaging to your upside not to expose APIs, you know, if you're a travel business, right? If you're selling flights, whatever it is, right? Tickets. If you don't expose it to these interfaces. Yeah, I mean, I can see myself not wanting to buy anything through that e-cart process anymore. Like add to cart and check out. No, I just want to say, buy it. Give me some options, do some research for me. Okay. I want that one, buy it for me. Right. [00:12:43] **Sinan**: If you don't expose an API how is that going to work? Yeah. Makes a ton of sense. It lines up for B2C very well lines up. You're a hundred percent right. [00:12:49] **Sagar Batchu**: Yeah, absolutely. I think it's really day zero of like a new ecosystem emerging. And I think for companies with APIs like you just want to be involved, right? It's moving so quickly that not being involved means you're going to miss out on. It's very hard to guess who the winners and losers are going to be, and I think the best thing you can do is kind of be in that stream, in that river as it's flowing. It's one kind of, you know, thing that I mention to a lot of companies we work with. ## Browser Automation as a Transition Technology [00:13:14] **Sinan**: Maybe I wanted to touch base on something. That's something that really plays into what you guys are building. So all this browser use models, right? Like computer use browser use models. They are an intermediary kind of a solution, a transitional solution, right? Because we're waiting for the APIs to be exposed. [00:13:32] **Sinan**: So nobody really genuinely loves the idea of an agent orchestrating a Chrome browser. Really, it's just a temporary point in time that we have to do it because like you said, like, sitemaps had to be invented for better SEO, right? Now we're waiting for the APIs to be kind of be in place to be able to drop that whole click and click orchestration stuff and direct access to the API. [00:13:53] **Sinan**: So it's a transitional phase. But it's also important because we don't have what we need, right? Once we have APIs on every direction that we look at every application that we want to interact with, expose those APIs, I think we're going to be in a great spot. So you are actually the destination, you know, so that's a good place to be at. ## Tool Discovery: The Next Frontier [00:14:13] **Sagar Batchu**: Yeah, no, absolutely. It's really early here. Like what I kind of see is that still a pretty big gap between like POC and enterprise usage. But yeah I do see that. I'm also seeing something similar and that people are asking, like, do you need a model for tool discovery? As well, right? [00:14:32] **Sagar Batchu**: Like models very specifically optimized for actually finding the right tools to work with. And as you said, like, if there is something like site map that appears, then there is a search problem. All of a sudden right now, there's no search problem because these ecosystems are small and it's just small little tool registries that people are running. But yeah, no, absolutely. [00:14:50] **Sinan**: Yeah, an API registry is a great idea. Tell me what you're capable of. Show me how it's done. Give me some examples and let me consume them in real time. I like that vision. And yeah, I mean, definitely you're building something that will become how we interact with the web going forward for sure. [00:15:06] **Sinan**: You know, as I said, like, it's not a transitional technology. It's where the destination is the end point. It's the end game, right? Yeah, but we do need that transitional period and in cybersecurity is endemic, by the way. You end up building a lot of features that are, well, you're building a lot of products that are going to be a feature of the platform in the fullness of time. [00:15:23] **Sinan**: There's no way avoiding that you have to build it. It's a need in the market, but, you know, very well, right? In 3, 5, 10 years out, it's going to be a feature of the bigger platform that you're overlaying, right? So transitional stuff, a lot of computer use, a lot of browser orchestration has to happen before API is take hold everywhere. ## Conclusion and Looking Forward [00:15:43] **Sagar Batchu**: Absolutely. Yeah, no, I agree. It's another analogy. I tell people, it's kind of like the autonomous way more cars on the street. Like, we're in an in between period where there's driverless cars and then drivers as well. But once you have all driverless cars, like, suddenly, I think you get new interfaces open up and like, you know, you can address the whole fleet of cars in one go make changes. [00:16:06] **Sinan**: We'll see where the POCs are going to materialize production use. But yeah, we're also seeing a lot of that. A lot of kick in the tires, a lot of excitement, but let's see if it's going to be really transformative. But I do believe, you know, we have to be optimistic. [00:16:18] **Sagar Batchu**: On that note, you know, we'll come to the end of this awesome chat. You know, and thanks so much for, joining us today. I know OpNova is doing some awesome stuff and you guys are just getting started. If people want to find out more about what you're doing there, where can they kind of go to reach out to learn more? [00:16:34] **Sinan**: OpNova.ai please fill out the contact form, depending on what kind of interest that you have. We can set you up. Thanks. This was super fun. Yeah. [00:16:41] **Sagar Batchu**: Thanks. ## Frequently Asked Questions ### What is OpNova and what does it do? OpNova automates back office functions for regulated industries, focusing on repetitive, mundane, and error-prone tasks, especially for companies that lack modern APIs. It helps bridge the gap between legacy systems and modern automation needs. ### How does OpNova bring automation to systems without APIs? OpNova leverages Robotic Process Automation (RPA) technology combined with multimodal AI models. These models record user actions, break them into chunks, understand the intent behind each action, and then create automation templates that can replicate those actions through clicks, text entries, and other interface interactions. ### What is MCP and why is it important? MCP (Model Context Protocol) is a standard developed by Anthropic for tool calling in AI systems. It's becoming an important standard for builders in tech centers but may need "bridges" to work with underserved industries that don't yet have modern API infrastructure. ### Why should companies care about having APIs in the age of AI? APIs are becoming essential infrastructure for participation in the emerging AI ecosystem. Without APIs, companies risk being excluded from tool discovery systems, AI agents, and other automation technologies. As Sinan explains, it's similar to how websites needed sitemaps to be discovered by search engines. ### What is the "API tax" problem? Similar to how Single Sign-On (SSO) was often locked behind expensive enterprise tiers (the "SSO tax"), some companies are now putting API access behind premium pricing tiers. This practice could hinder adoption and integration with the broader AI ecosystem, especially as APIs become necessary infrastructure rather than optional features. ### How will browser automation and API development evolve together? Browser automation (like what OpNova does) is seen as a transitional technology while waiting for universal API adoption. In the future, direct API access will likely replace browser automation for most use cases, but the transition period is necessary and could last several years. # rise-of-developer-infrastructure Source: https://speakeasy.com/blog/rise-of-developer-infrastructure We have a theory about what's going on with developer infrastructure: - The most successful engineering organizations in the industry have invested heavily in internal developer infrastructure, which has given them a differentiated advantage when it comes to constant innovation and speed of shipping products. - The next 10 years will see the majority of developers shifting from using cloud infrastructure to using developer infrastructure - Composable, Extensible and Usable: these will be the guiding principles for great developer experience. ## What is Dev Infrastructure Dev infrastructure can be a bit of a fuzzy gray area, but we define it as the tooling which forms the connective tissue between your application code and your cloud primitives. Your cloud provider defines the limit of what's computationally possible, your application code determines the particular what you're focused on, and your developer infrastructure determines how quickly you're able to get your application code running in production and how well you can maintain it atop primitives from your cloud provider. When it comes to sustained innovation in software development, **good** **developer infrastructure** is the secret weapon that gives the best companies a head start on their competition. It's not often talked about because it's often not the shiny apps your customers use, but it is critically important to velocity and quality. Developer infrastructure is a lot like public infrastructure: when it's working well you forget it's there, but when it's not working, you've got yourself a major problem. when it's not working, you've got yourself a major problem Over the last 10 years, developers at smaller companies have struggled to balance developing innovative apps while also managing the complexity of the cloud. Meanwhile, companies like Uber, Airbnb, Stripe, Netflix et al. have been able to invest heavily to build up in-house platform teams, which supports the continued productivity of their application developers. Some of the developer infrastructure those teams created were shared publicly. Two well knowns that I've personally adopted in high scale settings are: - [**Kubernetes**](https://kubernetes.io/) - The now ubiquitous container deployment and orchestration system was born from Google's famous internal [**Borg**](https://kubernetes.io/blog/2015/04/borg-predecessor-to-kubernetes/) system which provided abstractions for resource allocation. For most (stateless) use cases devs were able to “submit” a container and let K8's handle how it got running on VMs. Needless to say there is now a massive ecosystem in continuous evolution around K8s. - [**Spinnaker**](https://spinnaker.io/) - There were plenty of ways to build and deploy containers independently but they all required you to manage your own workflow. This meant brittle scripts, triggers, sometimes complete bespoke stacks maintained by internal teams. Spinnaker solved this with an orchestration first approach - a developer could string together many different steps which could live across team and system boundaries. Since then we've seen an [**explosion in this space**](https://landscape.cd.foundation/). Each of these was enthusiastically adopted by the developer community. At the time of their release, they each represented massive improvements over the status quo. Yet, this wasn't the end of the story. There were some issues. First off, it wasn't sustainable for devs to continue to rely on larger companies sporadically open sourcing their tools. Increasingly ambitious business growth targets meant that devs needed consistent innovation of tooling to enable them to build more, faster. And more importantly, the tools had been developed by large organizations with huge dedicated platform functions; they weren't optimized for application developers operating at a smaller scale without robust dev ops support. Simply stated, though the tools were powerful, they lacked a great dev experience. App devs were still having to go down the dev ops rabbit hole, spending far too much time trying to figure how to secure, deploy, and maintain their applications. ![BD about dev ops](/assets/blog/rise-of-developer-infrastructure/rise-of-developer-infrastructure-image-02.png) ## Who Will Build the Future of Dev Infra The way developers interact with the cloud is undergoing a profound shift, Erik Bernhardsson described it well in one of his recent blog posts, “[**Storm brewing in the stratosphere**](https://erikbern.com/2021/11/30/storm-in-the-stratosphere-how-the-cloud-will-be-reshuffled.html)”. The last 10 years were the initial phase of the cloud movement - shifting your compute and storage from your own server racks to those maintained by a commoditized public cloud. Now we are in the early days of the second phase - the abstraction of the cloud in order to undo the productivity loss caused by dev ops being splattered across the stack. We are pushing towards a future where every App and API dev will launch their product by focusing on the application primitives while maintaining the benefits of public cloud elasticity. That's why we're beginning to see the paradigm start to shift. As the cloud has matured, we are now beginning to see the development of dev infra atop the underlying cloud primitives. Some of the biggest drivers for this shift are: - **The desire for great UX**: Developers now expect their tools to not only be powerful, but also to be usable. A great UX means more personas in the org can insert themselves into the development loop, increasing leverage for the devs. A great dev tool is not only great UX but also great dev ex, as we explore below. - **The focus on differentiation**: Developer time has never been in higher demand. Businesses need to focus the efforts of their developers on solving the problems that differentiate their business. Everything non-mission critical, wherever possible, should be handled by a 3rd party. Tools that take unnecessary tasks out of development and deployment are invaluable. - **The dev infra gold rush**: The market is awash in solutions as startups armed with VC cash have rushed in to fill the need for cloud products with dev experience as the primary differentiator. For a dev looking to plug a gap in their development lifecycle or offload non-core work with 3rd party tooling, there have never been more options. - **Multi-Cloud**: As organizations begin to consider the implications of cloud lock in, it becomes critical that their dev tooling remains consistent across platforms. Optimizing an application for multiple clouds is a vastly inefficient use of developer time. Dev infra developed by cloud vendors is typically limited to just their cloud and vertically integrated. Cloud platforms rooted in developer productivity have seen rapid growth in the last few years. One of my favorite dev ex innovations recently is PlanetScale's [**Revert**](https://planetscale.com/features/revert) feature. A mention of database migrations can invoke anxiety in the most battle-hardened devs. Commit a stateless SQL file to manage a production database? No thank you! Another noteworthy mention is [**Cloudflare open sourcing their Workers runtime**](https://blog.cloudflare.com/workers-open-source-announcement/) - Devs can now deploy fullstack apps serverless-ly on the edge with just a click and with full transparency, amazing ! As this trend continues, the leverage of each developer will soar. For the majority of teams, dev ops will be a distant memory. We're approaching the inflection point. ## What Makes Great Dev Infrastructure Companies like PlanetScale and Cloudflare are at the tip of the iceberg. The community of independent dev tool companies has really begun to blossom in the last few years. The cloud providers commoditized access to secure, reliable, scalable server capacity, This, in turn has provided the foundation required for dev infra companies 100% focused on building tools with a great dev experience to be viable. As to what constitutes a great developer experience, it's still early days so there aren't many resources. However, when we're building our tools, we grade our work against our developer experience pyramid: ![Developer experience pyramid](/assets/blog/rise-of-developer-infrastructure/rise-of-developer-infrastructure-image-03.png) The Dev Ex Pyramid **Dev Infrastructure**: - **Usable** - An individual dev or team should be able to find the tool, set it up, and start using it without barriers or issues in 30 minutes. - **Composable** - The tool should be modular enough for developers to slot it into their (probably complex) environments. Support for multiple frameworks and integrations is key to adoption. - **Extensible** - Although most developers will only ever scratch the surface of a tool's functionality, every tool will have its power users. Power users should be able to add functionality to the tool, for advanced use cases. **Cloud Infrastructure (**These are table stakes!) - **Scalable -** Able to elastically scale compute to the needs of the application. - **Reliable -** This one is pretty easy. If a tool doesn't have 99.99% reliability, then people won't use it. If you are building infrastructure, you have to be dependable. - **Secure -** You cannot jeopardize your client's security. Your tools must be water tight. For us this means running within our client's cloud environment and minimizing any external network communication. We constructed this pyramid based on our experience working on enterprise software, and the things we valued in the tools we used. We would love to hear from other developers to see if this is in line with what they think is important. Are there things we've missed? We'd love to know. The future of dev infra is bright, and we look forward to making our contribution. [**For any devs who want to pitch in and help us build great infra for the developer community, please check out the job board. We would love to hear from you!**](https://jobs.lever.co/speakeasyapi/) # rss.xml Source: https://speakeasy.com/blog/rss.xml {/* export const getServerSideProps = getRssServerSideProps; */} # Ruby Enumerator support for pagination Source: https://speakeasy.com/blog/ruby_beta_release Ruby has been an enduring staple of web application development over the last decade, and Speakeasy is thrilled to announce that Speakeasy's Ruby SDK Generation is now available in public beta. This marks a significant milestone in our mission to provide developers with powerful, elegant SDKs across all major programming languages. Key highlights: Idiomatic Ruby design with method chaining and blocks Built-in OAuth 2.0 flows with automatic token management Enhanced error handling with custom exception hierarchy Intelligent pagination for handling large datasets ## Why Ruby matters in 2025 With Ruby powering millions of web applications and the continued innovation in the Ruby ecosystem, Ruby remains a cornerstone of modern web development. The language has evolved from its dynamic roots into a mature platform for building scalable, maintainable applications, with Ruby 3.x introducing features that enhance performance and developer productivity: ### The evolution of Ruby's elegance #### Ruby 1.8 - Basic dynamic typing ```ruby def process(data) # ... end ``` #### Ruby 2.0 - Keyword arguments and refinements ```ruby def process(data:, logger: nil) # ... end ``` #### Ruby 3.x - Pattern matching and improved type annotations ```ruby def process(data:, logger: nil) case data in { success: true, result: Array => results } results in { success: false, error: String => error } raise ProcessingError, error end end ``` Modern Ruby offers a balance between developer productivity and enterprise-grade reliability, making it a great choice for today's development teams. Our beta release brings production-ready Ruby SDK generation to this thriving ecosystem, enabling API providers to deliver exceptional developer experiences to their Ruby users. ## What's new in our beta release Our Ruby SDK generation includes powerful new capabilities: ### Core features Idiomatic Ruby: Leverage Ruby's expressive syntax with proper method chaining and block support Typing: First-class support for Sorbet type system, with the flexibility to disable for performance-oriented projects. OAuth 2.0: Implement secure authentication flows with built-in token management Deep Object Support: Pass complex nested objects in query parameters without serialization headaches Minimal Dependencies: We use only what's necessary to keep your dependency tree lean ### Developer experience improvements Smart Pagination: Navigate large datasets with Ruby Enumerators that handle pagination automatically Resilient Networking: Implement robust retry mechanisms for handling transient API failures Extensible SDK Hooks: Customize request/response workflows using Ruby blocks for logging, metrics, and more Comprehensive Docs: Access usage examples for every endpoint with copy-pastable code samples These optimizations ensure that your API integrations remain fast and reliable, even at scale. ## Core SDK Features In-Depth ### Idiomatic Ruby Design Ruby's philosophy of developer happiness and expressiveness shines through in our SDK design. We've embraced Ruby's conventions to create SDKs that feel natural to Ruby developers. Here's how our idiomatic approach translates into real code: ```ruby class Payment extend T::Sig include Crystalline::MetadataFields field :id, ::Integer field :amount, ::Float field :currency, Models::Components::CurrencyEnum field :status, Models::Components::StatusEnum field :created_at, ::Date field :description, Crystalline::Nilable.new(::String) field :owner, ::String sig do params( id: ::Integer, amount: ::Float, currency: Models::Components::CurrencyEnum, status: Models::Components::StatusEnum, created_at: ::Date, owner: ::String, description: T.nilable(::String) ).void end def initialize(id:, amount:, currency:, status:, created_at:, owner:, description: nil) @id = id @amount = amount @currency = currency @status = status @created_at = created_at @description = description @owner = owner end end ``` This approach ensures that your code feels natural to Ruby developers, with familiar patterns like question mark methods for boolean checks and expressive method names. Thanks to our optional support for Sorbet, method signatures can be generated with 100% complete typing. ### Pagination ```ruby sdk.subsdk.do_multipage.each do |page| # SDK handles pagination automatically process_page(page) end ``` ### High Accuracy Numbers For financial and scientific applications, supporting high-accuracy numbers in your SDK is a must. With Speakeasy's Ruby SDK, we support working with arbitrary precision numbers at all times in the SDK. When serializing/deserializing, the maximum precision is dictated by JSON, and Ruby does not currently support encoding arbitrary precision numbers as strings, so some precision may be lost when encoding/decoding objects. ### OAuth 2.0 Implementing OAuth flows correctly can be challenging. Our Ruby SDKs now include partial support for OAuth 2.0, making secure API integration straightforward. ## Get Started Today Ready to deliver a world-class developer experience to your Ruby users? Here's how to get started: [Generate your Ruby SDK in less than 5 minutes](https://www.speakeasy.com/docs/sdks/create-client-sdks) [Read the documentation for configuration options](https://www.speakeasy.com/docs/languages/ruby/methodology-ruby) Ruby has evolved into a powerful, expressive language with a mature ecosystem, and with our SDK generator, you can now provide your users with a developer experience that leverages all these capabilities. Try it out and see how it can simplify your API integration workflow. # rust-aws-api-usage-visualization Source: https://speakeasy.com/blog/rust-aws-api-usage-visualization ## New Features - **API Usage Dashboards \[Embed\]** \- Make it easy for your developers and API users to understand API errors and traffic patterns with our out-of-the-box usage dashboards. With a click or two, users can see data across various time spans, and group by API endpoints, customer, and status code. The dashboards are of course available as an embed that can be incorporated into your own developer portal or any customer facing app. [Watch Chase walkthrough the new embed!](https://www.loom.com/share/637bbadcbdba499e949a4ec86ca39246) - **Rust SDK** - Speakeasy is now available to anyone building their API stack in Rust! The new SDK has full support for Speakeasy features like field masking, path hints, and more. Check out [the full SDK in our public github repository.](https://github.com/speakeasy-api/speakeasy-rust-sdk) - **Self-Host on AWS** - If AWS is your cloud of choice, you can now run Speakeasy! Our solution is now fully operational for those who want to self-host on AWS. ## Incremental Improvements & Fixes - **Delete API Endpoints** - No more clutter! You can now remove APIs and API endpoints you've added to your Speakeasy workspace. # sass-vs-css-modules-vs-css-in-js Source: https://speakeasy.com/blog/sass-vs-css-modules-vs-css-in-js We are in the midst of building the first version of our API Ops platform. One of the components which we are actively building is a developer console where devs can get an overview of their APIs and each endpoint's performance. While building our UI, we are debating many of the fundamental front end architecture decisions that will shape our UI development. One of the biggest debates thus far was whether we would use SASS or CSS-In-JS for our styling. We know developers are facing these same questions every day, so we want to publish our thoughts in case they are useful to anyone else. Ultimately, we chose to move forward with CSS-In-JS as our primary styling mechanism for reasons specific to our business; we considered it important for the UIs we build to be easily embeddable within external UIs. We felt that CSS-In-JS was the best option for embeds, because integrators wouldn't need to worry about dealing with style injection, and could theme our components into their style. We'd love to know your thoughts and how you reached decisions about styling your UI? Have you had any bad experiences with CSS-In-JS? Below is our full internal conversation only minimillaly edited for brevity: ![Internal conversation about CSS-in-JS part 1](/assets/blog/sass-vs-css-modules-vs-css-in-js/sass-vs-css-modules-vs-css-in-js-image-01.png) ![Internal conversation about CSS-in-JS part 2](/assets/blog/sass-vs-css-modules-vs-css-in-js/sass-vs-css-modules-vs-css-in-js-image-02.png) ![Internal conversation about CSS-in-JS part 3](/assets/blog/sass-vs-css-modules-vs-css-in-js/sass-vs-css-modules-vs-css-in-js-image-03.png) ![Internal conversation about CSS-in-JS part 4](/assets/blog/sass-vs-css-modules-vs-css-in-js/sass-vs-css-modules-vs-css-in-js-image-04.png) ![Internal conversation about CSS-in-JS part 5](/assets/blog/sass-vs-css-modules-vs-css-in-js/sass-vs-css-modules-vs-css-in-js-image-05.png) If you're interested in learning more about [what we're building](https://www.speakeasy.com/), please reach out! — Transcript of Convo for accessibility purposes :) **Fullstack dev 1** \> @UX Designer and I were just talking about the UI and I think it might be time to move away from some of the MaterialUI components to make it a little easier to get things looking as he's designed. I'd like to move most of the styling into sass. Anyone have thoughts on that? **dev** \> what is sass ? Is that another library ? **Fullstack dev 1** \> it's a "language" that transpiles to css: [https://sass-lang.com/](https://sass-lang.com/) **UX Designer** \> yes, like a css framework that allows smart functionality over and above vanilla css **Fullstack dev 1** \> it makes it a little easier to have a consistent theme **Tech Lead** \> interesting - is this any different than using tailwind and related libraries ? **Tech Lead** \> another question i'll ask is if we move away from MUI will we need to build our own modals, drawers etc ? I would be wary of introducing a lot of work around maintaining our own design system without being able to leverage tablestakes libraries under the hood (edited) **UX Designer** \> Could we use MUI partially, for the core components we don't need to redesign (like modals) **Fullstack dev 1** \> I think tailwind is sort of like bootstrap **Fullstack dev 1** \> @UX Designer yeah, I was definitely going to start with the components that don't have a great MUI analog (or at least that don't style as well) **UX Designer** \> but then do some custom like for buttons and other more visual/branding components **Fullstack dev 1** \> right **Tech Lead** \> Having our consistent branding and style is important so our users feel they're using a polished product but i would strongly reconsider spending any time on creating custom components for undifferentiated features (buttons, modals etc) and use that sparingly for differentiated features (request viewers, test automation components etc) **Fullstack dev 2** \> IMO Material UI / SASS are totally interoperable. Material UI is good in that it's a sensible set of defaults, but for anything designed it can be just ignored, or themed to match. Similarly if the styling is via SASS via \[styled-components, MUI/Emotion, tailwind classes\] it doesn't matter too much: it's just CSS at the end of the day. I have biases against globally-running CSS rules, because it's slightly harder to maintain (as they start to overwrite each other): but that just depends on how SASS is compiled/injected into the components. On a more general level, I personally have biases towards adjusting design to fit a standard component library rather than be more custom in each view. E.g. I'd rather we style MaterialUI, rather than do our own custom CSS, just because it's faster and easier to mutate in future. **Fullstack dev 1** \> ok **Tech Lead** \> i think the idea of using SASS is a good one but i think we should probably revisit the option every few weeks. Still feels premature to maintain our own components until we find a novel ux interaction that standard components don't support **UX Designer** \> ok so the implications of this is that some of the styling in the UI may not reflect identically what we see in the live UI, but it will be minimal and inconsequential **UX Designer** \> As long as branding colors and fonts are custom (which they are in MUI), we should be largely ok **Tech Lead** \> Sounds good, thanks raising this Fullstack dev 1 and UX designer. Lets pushing MUI as long as possible here to reduce our maintenance burden. **Tech Lead** \> @**Fullstack dev 1,** @**Fullstack dev 2** I was thinking about this from another angle - it seems like if we have a Master CSS style sheet there is some value in using SASS right now (mostly from a developer productivity point of view). If we use styled components i see that value going down. The other thing to think about is we do have downstream in the product roadmap the need to inject parts of our developer dashboard (as iframe or embeds) into company's own dashboards. It seems like going the way of styled components would keep the door open for that. Curious to hear you thoughts **Fullstack dev 2** \> IMO: 1. If we leave CSS-In-JS and go down the route of a Global CSS style sheet (e.g. via SASS), I'm not convinced we gain any short-term developer productivity benefit. The only time I'd personally recommend this nowadays is when we have CSS-only developers, who do not want to touch JS. My experience is that this gain still would often lose to other complexities better-handled in a CSS-In-JS approach in the medium-term \[0\] \[1\]. 2. If we go down any of the CSS-In-JS routes (e.g. styled-components, mui/emotion), we will be easier to embed, as the developer embedding us will not need to think about style injection. Currently we use emotion (as MUI wraps it): [https://emotion.sh/docs/introduction](https://emotion.sh/docs/introduction) . Emotion is essentially a more modern version of styled-components: it provides a superset of features, and is a bit faster \[2\]. 3. If we continue down a MUI-theme route, we become easier to re-style in one place. E.g. we could more easily be styled in customer colours. Similarly implementing dark mode is trivial, if we want that. \[0\] I personally have a little Trauma from giant ball-of-mud global CSS stylesheets implementations. In a couple 2-year length projects we went down this route, but gradually spent more-and-more time hunting down bugs caused by CSS overriding each other all over the app. In each project, there were several “CSS refactor” mega-epics to try to trim the mud into something sensible. They always failed, and were very time-expensive. I don't think this is the suggestion, but wanted to state my opinion and validate consensus that we should rule this out. \[1\] An alternative that is closer to CSS is CSS Modules (which can be powered via SASS). I've also used this on one major project. I had no negative experience, but features which required dynamically adjusting theme (e.g. implementing dark mode) were expensive to implement, and so we never bothered. For a devtools project I think theme adjustment (either by adding dark mode, or by allowing a customer to theme us on an embed) is probably important. Hence would rather go with a pure CSS-In-JS solution. \[2\] Of the CSS-In-JS libraries, I just picked emotion as it's IMO the most modern of them, and it's also the MUI default. The API to all of them is effectively the same. No strong opinions here. (edited) # sdk-and-terraform-generation-improvements Source: https://speakeasy.com/blog/sdk-and-terraform-generation-improvements ## 🚢 Improvements and Bug Fixes 🐛 #### Most recent version: [Speakeasy v1.160.0](https://github.com/speakeasy-api/speakeasy/releases/tag/v1.160.0) 🚢 Vacuum reports to validation output\ 🚢 feat: added validation for invalid const/default values\ 🐛 Ensure serialization method suffix is retain when overriding method names ### Typescript 🚢 Add support for server sent events (SSE)\ 🐛 Correctly resolve union references and imports\ 🐛 remove Type from reserved words ### Java 🚢 Use primitives for required boolean/numeric values\ 🚢 support const (primitives only, not operation parameters)\ 🚢 support file streaming for downloads\ 🐛 prevent namespace collisions with `java.lang.Object` ### Python 🐛 Correctly resolve union references and imports short global and server variable names and conflicts with reserved type keyword\ 🚢 Add support for server sent events (SSE) ### Go 🐛 Ensure global security callbacks are always an option\ 🚢 Add support for server sent events (SSE)\ 🚢 bump minimum go version to 1.20 ### C# 🚢 Add pagination section to readme\ 🚢 add authentication readme section\ 🚢 implement global security flattening\ 🚢 support applying Bearer prefix for oauth2 and oidc scheme\ 🐛 fixed handling of sdks without servers\ 🐛 `serverIndex` not used during SDK instantiation ### Terraform 🚢 default value for big floats (type: number)\ 🚢 push default values into Computed + Optional fields explicitly; such that it's available as terraform plan output\ 🐛 type reconciliation of enums + non-enums could sometimes cause attributes (e.g. computed) from not being applied ### Ruby 🐛 imports and module issues fixed # sdk-best-practices Source: https://speakeasy.com/blog/sdk-best-practices ## From API-First to SDK-First One of the most significant trends in tech over the past ten years has been the proliferation and success of API-as-a-product companies (e.g. Twilio, Algolia, etc). But the term API-as-a-product, obscures one of the most critical ingredients in these companies' success. The secret that the most successful API companies have been hiding in plain sight, is that their APIs are not the actual interface beloved by users. It is their SDKs which are at the heart of the best-in-class developer experience these companies offer. If you want to delight developers, don't try to just be API-first, focus on becoming SDK-first. So why doesn't every API company offer SDKs to their users? Up until now, it's been really hard to sustainably build great SDKs. If you look at a list of [the most popular APIs](https://www.postman.com/explore/most-popular-apis-this-year), you'll find that even some of the biggest API companies have failed to build out robust SDK programs. Many offer patchy or incomplete support. Change is coming though. Speakeasy is committed to giving every company access to an API experience platform on par with what the best API companies provide. A major component of that platform is a workflow to easily and sustainably build SDKs for all your REST APIs. In this piece, we'll discuss why SDKs are important, what qualities make a great SDK, and how to overcome the common problems that typically plague SDK development and maintenance. ## Why Are SDKs Important? SDKs provide numerous unique benefits to your end-users' developer experience, and to your team too: - **Reduce your team's support burden**: By enabling type definitions in the IDE, SDKs reduce the likelihood of user error during user integration. That in turn means less support tickets for your team to manage. - **Improve product discoverability**: SDKs put your product in the place where developers are most likely to look for solutions. Client libraries on Github and in popular package managers will help your product develop social proof as users encounter you in familiar environments. - **Increase revenue with a larger addressable market**: Every SDK you offer makes it easier to appeal to a new community of developers. That means an increase in users, and ultimately, more revenue. Of course, releasing a badly-written SDK could do more harm than good. Let's dive into what it means to build a great SDK.
![Dank meme showing how SDKs improve your API](/assets/blog/sdk-best-practices/sdk-meme.png)
## Best Practices for a Great SDK We've talked to hundreds of developers on this topic. While there is always a bit of personal preference, these are the things that every developer wants to see in an SDK: ### 1. Type Safe Type safety might be the most important aspect of SDK creation. By making your API's inputs and outputs explicit, the developers integrating the API into their application can better understand how the API is intended to be used — and massively reduce the amount of incorrect requests being made to your API. Type safety will help developers debug in their IDE as they write the application code, and spare them the frustration of having to comb through the constructed data object to see where/ mistakes occurred. The more that you can include in your type definition, the more feedback developers will have when they are building with your SDK. The below example shows how Speakeasy uses Zod Schemas to define the input and output types for a product object in TypeScript. This allows developers to validate the input and output of the API at runtime, and provides a clear contract for how the API should be used: ```tsx filename="product.ts" export namespace ProductInput$ { export type Inbound = { name: string; price: number; }; export const inboundSchema: z.ZodType = z .object({ name: z.string(), price: z.number().int(), }) .transform((v) => { return { name: v.name, price: v.price, }; }); export type Outbound = { name: string; price: number; }; export const outboundSchema: z.ZodType = z .object({ name: z.string(), price: z.number().int(), }) .transform((v) => { return { name: v.name, price: v.price, }; }); } ``` ### 2. Abstracted SDKs spare your users from having to worry about the minutiae of how your API works. You can abstract away details like: - **Networking code**: SDKs can handle the details of making network requests to an API. This allows developers to focus on the functionality they want to implement, rather than the details of how to communicate with the API. - **Request and response formatting**: SDKs can handle the details of formatting requests and parsing responses in a format that is specific to the API - **Error handling**: SDKs can interpret the details of errors that occur when using an API, allowing developers to focus on their application logic rather than error handling. ### 3. Human Readable This one is pretty self-explanatory. AI hasn't made developers obsolete yet, so there is going to be another person on the other side of your SDK code. When code is well-organized, well-commented and easy to read, developers are more likely to be able to understand how the SDK works and how to use it correctly. ### 4. Limited Dependencies Codebases are jungles, and SDKs are another component of this complex ecosystem. When an SDK has a large number of dependencies, it can be more difficult to use the SDK with other libraries and frameworks. If a developer has to try and resolve incompatible dependencies before they can use your SDK, there is a high risk they will abandon the API integration altogether. Limiting the number of external dependencies in your SDK is therefore critical to ensure compatibility. ### 5. Enterprise Features We think that features like retries, pagination, and security helpers should come in every SDK. These aren't things that are strictly required at the earliest stages of an integration, but as soon as users want to use your API for production use cases, this matters — and their absence can slow down integrations significantly. It's just another case where a user doesn't want to have to think about how to do this well — and may not have enough product context do to so optimally. The API creator is in a much better position to set sensible defaults here. ```tsx x-speakeasy-retries: strategy: backoff backoff: initialInterval: 500 # 500 milliseconds maxInterval: 60000 # 60 seconds maxElapsedTime: 3600000 # 5 minutes exponent: 1.5 statusCodes: - 5XX ``` ### Language Idiomatic This is a meaty topic, and it's hard to discuss in generalities (it's different for every language), so let's walk through an example of the type of design choices that a developer might expect of a Go SDK: - **Minimal dependencies** and relying on the Go standard library as much as possible. - **Struct tags and reflection based (de)serializers** to define how the types we generate are correctly serialized based on the OpenAPI document. - **Pointers for optional objects** including fields/parameters/response and request bodies to ensure that the user can differentiate between a field not being set and a field being set to a zero value. - **A Utils package** that improves readability by bundling the methods for configuring the SDK and serializing/deserializing the types we generate into a shared package, avoiding the need to duplicate in each method. ```go package shared type PetStatusEnum string const ( PetStatusEnumAvailable PetStatusEnum = "available" PetStatusEnumPending PetStatusEnum = "pending" PetStatusEnumSold PetStatusEnum = "sold" ) type Pet struct { Category *Category `json:"category,omitempty"` ID *int64 `json:"id,omitempty"` Name string `json:"name"` PhotoUrls []string `json:"photoUrls"` Status *PetStatusEnum `json:"status,omitempty"` Tags []Tag `json:"tags,omitempty"` } ``` If you make sure your SDK is type safe, idiomatic, and compatible with their existing environment you're going to attract developers and inspire loyalty. At this point the last thing that needs to be solved is building a sustainable program for your SDK development. ## How to Sustainably Build SDKs When you start building SDKs you will quickly run into two major issues: 1. There are a lot of languages that you'll need to build support for 2. You need a way to update SDKs as your API changes. Let's walkthrough how you can overcome these two blockers. ### The Long-tail of languages and runtimes In 2022, [the proliferation of programming languages accelerated](https://octoverse.github.com/2022/top-programming-languages) — and there's no sign of that trend abating anytime soon. That's great for nerding out over language design, but it's a pain if you're on the hook for building your API's SDKs. Whereas 15 years ago you could have covered most programmers with 3-4 libraries, it's now probably closer to 8-10. Languages can also have multiple popular frameworks, each of which require idiosyncratic tweaks to get the developer experience correct. This fragmentation of languages & runtimes makes it harder to provide the same level of service to your users and makes it hard to keep track of how users are interfacing with your product. ![Chart showing the diversfication of server-side languages](/assets/blog/sdk-best-practices/popular-languages.png) It's not reasonable to expect that every company will have the language expertise internally to be able to support every language & runtime. That's why we've built the Speakeasy generator. We are committed to building out support for every popular language & runtime so that you don't have to. And in cases where people need something very specific, we offer the ability to right a custom template for the generator to use. ### Preventing SDK Drift Too many companies think of building SDKs as a one-time project. They assign developers or contractors to handroll SDKs in the languages they know best, and punt on a long-term support system. This works for a while, but inevitably an expanding API definition, and an increasing number of libraries saddles the engineering team with a constant stream of tedious refactoring work. This refactoring may or may not be prioritized leading to SDKs with divergent behavior. The best API companies have built SDK generators and workflows that update the SDK automatically as part of their CI/CD pipeline. Whenever a new version of the API is published, a new version of their SDKs is released. This is a huge lift for any company to undertake — which is where Speakeasy comes in. With Speakeasy, any development team can have the same API infrastructure as the world's best API companies. Producing idiomatic SDKs as part of your CI/CD is now available with our generator and a simple Github action: ## Final Thoughts What's considered table stakes for developer experience has never been higher. And as the primary interface for APIs, your SDKs are perhaps the single most important component of your developer experience. At the same time, the proliferation in languages & runtimes being used in production applications, means it's never been harder to support the developer community. When you are building SDKs, make sure you build something that developers will actually want to use. That means making sure your SDKs are type-safe and idiomatic. Finally, make sure that your SDK development is sustainable. Make sure you have a plan to provide ongoing support to the SDKs you build, otherwise you risk developers losing trust in the product when SDKs are not at parity with the API. If you want your to make your SDK creation and ongoing support easy to manage, consider trying out the Speakeasy pipeline. # sdk-docs-in-beta-a-2024-sneak-peek Source: https://speakeasy.com/blog/sdk-docs-in-beta-a-2024-sneak-peek Welcome to the first Speakeasy changelog of 2024! 🎉 We're excited to share the updates we snuck in at the end of 2023 and also give you a sneak peek of what's coming up in 2024. Sound good? Ok, let's go! 🚀 ## SDK Documentation in Beta Your users are code-native. Your docs should be too. Today, we're releasing our SDK docs product into Beta! What does code-native mean? It means fully integrating your SDKs into your documentation so that your documentation reflects how users will actually interact with your API. Let's face it: the best way to code is to copy and paste. ![API docs with SDKs featured](/assets/changelog/assets/sdk-docs.jpg) Some of the highlights from the beta launch include: - Code snippets that are compilable and feature the latest version of your SDKs; - Request & response objects presented in terms of your SDK's type system; - Generated sections for authentication, pagination, error handling, and more; - Easily customizable theming; - Deep linking & single page scroll so that you can navigate and share any level of your API reference; - Built on best-in-class open-source tools like MDX, Next.js, and CodeHike. Read more about it [in our release post](../post/release-sdk-docs). ## What's Coming Up in 2024 When we set out to build Speakeasy, we saw that dev teams everywhere were spending more time on API tooling than building the API itself. Let's face it: REST APIs are unruly. They neither have the flexibility of GraphQL nor the standardization of gRPC. However, they're still the simplest way for companies to expose their data and services to the world. We started our journey addressing the needs of API producers by taking on the burden of building the various interfaces needed to consume APIs. This experience has solidified our mission of making APIs everywhere easy to create and consume. This year, we're continuing our journey by moving upstream and providing API producers with all the tooling they need to build great APIs. Great external DevEx starts with great internal DevEx. New features to look out for: - More great SDK generation targets, including v2 Java generator, v2 of our Terraform generation to support multiple API calls per resource, and GA versions of Ruby, PHP, C#, and Kotlin; - "Level 2 SDKs" - chain calls in your client libraries; - A central registry to track and manage your API definitions and SDK packages; - Visibility of API changes across your organization; - Ability to define your own governance policies and enforce them on various artifacts. Onward and upwards! 🚀 ## New Posts: As you know, we love writing about all things API design, DevEx, and OpenAPI. Here are our latest blog posts. - [Working with Webhooks and Callbacks](../post/openapi-tips-webhooks-callbacks): Since OpenAPI3.1, the new `webhooks` keyword is a top-level element alongside paths. Webhooks are a mechanism that allows an API to send real-time data to a user as soon as an event occurs (without requiring the user to take any action). The user simply needs to subscribe to the event stream and provide a URL to start receiving data. These can now be described in your OpenAPI document alongside your regular APIs, enabling all API models to be described in a single place. - [Speakeasy SDKs vs OpenApi-Generator OSS](../post/compare-speakeasy-open-source): The pre-existing OpenAPI generator ecosystem has been a great inspiration for us, but we've felt the quality and flexibility of code gen is short of what's needed for production APIs. We've recently redone our Speakeasy vs OSS comparison to highlight where we've gone the extra mile in bringing you best-in-class SDKs. ## 🚢 Improvements and Bug Fixes 🐛 #### [Speakeasy v1.136.3](https://github.com/speakeasy-api/speakeasy/releases/tag/v1.136.3) 🚢 Support for OpenAPI-based ordering in generated SDKs\ 🐛 Improved server selection and retry usage snippets\ 🐛 Comparison of extensions when merging schemas ### Typescript 🚢 Support for extensionless import\ 🐛 Missing imports for form-encoded requests\ 🐛 Support for async iterators\ 🐛 Support for default and const values ### Go 🐛 consts and defaults with an optional attribute ### Terraform 🐛 consts and defaults with an optional attribute ### Python 🚢 Nullable support\ 🚢 Support for configurable license file\ 🐛 More robust retry connection logic ### Java 🚢 Oauth support through security callbacks\ 🚢 Javav2 released into preview in CLI. Try `--lang javav2` when generating SDKs\ 🐛 Fixed gradlew file permissions ### SDK Docs 🚢 Improved page performance for large OpenAPI specs\ 🚢 Support for language-specific sections and links\ 🚢 Support for rate limit tables\ 🚢 Support for Speakeasy error extensions # sdk-hooks-openapi-reference-more Source: https://speakeasy.com/blog/sdk-hooks-openapi-reference-more import { Callout, ReactPlayer } from "@/lib/mdx/components"; We've got a jam packed changelog that includes the introduction of new extensibility into our code gen platform and a comprehensive OpenAPI reference guide. Let's get into it 👇 ## SDK Hooks Since Speakeasy started, we've carefully balanced the extensibility and dependability of our code generator. We want users to customize their SDKs without creating a maintenance burden. It can be a difficult balance to strike. Which is why we're excited to announce the biggest addition of extensibility to our platform yet – SDK Hooks! With SDK Hooks, you are no longer constrained to what is defined in your OpenAPI document. You can now safely inject custom logic into your SDKs where you need it. ![Diagram showing SDK Hooks](/assets/changelog/assets/sdk-hooks.png) [Read more here](/post/release-sdk-hooks) ## OpenAPI Reference OpenAPI was designed to be capable of describing any HTTP API, whether that be REST or something more akin to RPC-based calls. By design, it has **a lot** of flexibility baked-in. That is great, but it makes it really hard to grok if you're new to the format (and often even when you're experienced). That's why we built the reference documentation that we wished we had when we were starting out. It's a comprehensive guide to the OpenAPI format, with examples, explanations, and links to the relevant sections of the OpenAPI specification. And even better, it's AI-enabled! Ask any OpenAPI question, and the reference guide will get you sorted out.
[Check out the full reference here](/openapi) ## 🚢 Improvements and Bug Fixes 🐛 Based on most recent CLI version: [**Speakeasy v1.231.0**](https://github.com/speakeasy-api/speakeasy/releases/tag/v1.231.0) 🚢 Offer sample OpenAPI Document during quickstart \ 🚢 Make example generation much, much faster \ 🚢 Generate code samples in speakeasy run \ 🚢 New CLI command: `speakeasy openapi diff`\ 🚢 Validate `gen.yaml` during speakeasy run ### TypeScript 🐛 Fix hoisting operation security and TypeScript basic auth support ### Java 🚢 Add `wait`, `notify`, `notifyAll`, `clone` as reserved words ### Python 🚢 Add support for additional metadata url ### Terraform 🚢 Capability to wrap classes and unions \ 🚢 Mix wrapped with non-wrapped resources in a multi-operation terraform resource \ 🐛 Read data sources during multiple read requests dropping unnecessary attributes ### C# 🚢 Introduce conventional namespaces into C# & Unity # sdks-and-our-march-of-progress Source: https://speakeasy.com/blog/sdks-and-our-march-of-progress SDK generators are attractive because they're easy. Hand Rolling SDKs requires time and broad language expertise. The downside has always been that generated SDKs sacrifice developer experience. Generated SDKs feel like they exist so that someone could tick a box and claim they offered SDKs, even though those SDKs would never be something a developer would want to use. We're determined to make an SDK generator that is not only easy to use, but makes SDKs that are great for your users. Since we rolled out the beta of our SDK generator, we've continued to add features that add the type of finishing touches that take an SDK from usable to enjoyable. ## New Features **Easy OpenAPI Extension: Multiple Servers** - OpenAPI is great, but it has some glaring holes. One is that, when there are multiple servers, it doesn't provide a strongly typed way to define which server to use by default. Speakeasy provides an extension to the OpenAPI spec that allows you to define an ID for each server in the Servers array. This can be done using the x-speakeasy-server-id property on the server object. [Read more about how in our documentation](/docs/customize-sdks/). ```yaml openapi: 3.0.3 info: title: Example version: 0.0.1 servers: - url: https://prod.example.com # Used as the default URL by the SDK description: Our production environment x-speakeasy-server-id: prod - url: https://sandbox.example.com description: Our sandbox environment x-speakeasy-server-id: sandbox ``` Servers are just the start. We're building out extensions for retries and pagination and are [looking for customers interested in being alphas users](https://app.speakeasy.com/). **Readmes & Code Comments** - SDKs are more than just the functions in the library. They're also the business context in which they exist. That's why the Speakeasy generator creates a Readme with install instructions and usage examples, and generates code comments & usage snippets based on your OpenAPI operation's descriptions. Read more about how in our documentation: [Readme & Code comments](/docs/customize-sdks/). **Custom HTTP Client Support** - We know that SDKs don't exist in a vacuum. That's why our SDKs are built to be optionally used with a custom HTTP Client. This allows you to use HTTP Clients that are setup to use proxies, provide custom telemetry or be preconfigured with global headers or any additional configuration. [Read more about how, in our documentation](/docs/customize-sdks/). ## Small Improvements Google Login - Users now have the option of logging in with their google account in addition to github. More auth providers to come! # seamlessly-managed-sdk-versions Source: https://speakeasy.com/blog/seamlessly-managed-sdk-versions Updating an API is a harrowing task. There's plentiful opportunity to introduce issues that could lead to unintended functionality in your product. So why make it even harder on yourself by then hand rolling the changes out to all of your client libraries in a variety of languages? At that point, you're just asking for there to be inconsistencies between SDKs that require painful client migrations to address in the future. That's why Speakeasy is making it easy with automatic generation & versioning of your API's SDKs. All you need to do is review and approve. ### **New Features** **PRs for New Versions** - Speakeasy provides both a github workflow & action to automate SDK creation. When a new version of your OpenAPI spec is published, we'll create new commits on a PR branch with the changes for each SDK and then an associated Github release upon merge. We can even automate publishing to package managers. If you're interested in end to end automation for your SDK management, get in touch. # self-serve-sdk-creation-pipeline-alpha Source: https://speakeasy.com/blog/self-serve-sdk-creation-pipeline-alpha Even though we are a team of lovely people, we understand that not everyone wants to talk to us to get their SDK creation pipeline set up. That's why we're hard at work building a self-serve pipeline. With just a few simple clicks in our platform UI, production-ready SDKs will magically appear in your Github. If you want to see the final result, check out this [Python SDK repo](https://github.com/speakeasy-sdks/openai-python-sdk) for OpenAI API, and its [PyPi package](https://pypi.org/project/speakeasy-openai/). We're currently running user feedback sessions, so if you're interested in getting your hands on this new feature ASAP, [sign up for a testing slot](https://calendly.com/d/yy2-6r6-3d4/alpha-test-latest-speakeasy-release) before they fill up! ## New Features **Self-serve SDK Creation Pipeline:** You can now go from spec to published package in a couple clicks. Our pipeline runs as part of your CI/CD, so you'll never need to worry about SDKs getting out of sync with your spec again. To get started, all you need to do is: 1. Upload your OpenAPI spec. 2. Select your target languages. 3. Optionally add publishing information. 4. Click ‘Create' — thats it ! Check it out in action below: ## Improvements **Publishing for Java**: You can now publish your Java SDKs to Maven. Just add your Maven API key and start publishing! **Test coverage for SDKs**: SDKs are a critical product surface area, and we treat them as such. We've revamped our test coverage and developed a set of language specific test suites to prevent any regressions in the SDKs we create. # sensitive-data-masking Source: https://speakeasy.com/blog/sensitive-data-masking ### New Features - **Mask sensitive data** - Whether it's request/response fields, headers or cookies, use the middleware controller to prevent sensitive fields from entering the platform. Alternatively, ignore entire routes by not assigning the Speakeasy middleware to the endpoint's router. ```go func MyHandler(w http.ResponseWriter, r *http.Request) { ctrl := speakeasy.MiddlewareController(req) ctrl.Masking(speakeasy.WithRequestHeaderMask("Authorization")) // Masked header // the rest of your handlers code } ``` - **Get a customer-specific view** - When you're looking at API logs, you need to be able to filter down to a single customer. By configuring the Speakeasy SDK to pick up your customer-key, you can easily breakdown to get a customer-specific view. ```go func MyHandler(w http.ResponseWriter, r *http.Request) { ctrl := speakeasy.MiddlewareController(req) ctrl.CustomerID("a-customers-id") // Specify customer ID // the rest of your handlers code } ``` ### Smaller Changes - **Easy service discovery** - Search APIs registered with the Speakeasy platform via associated metadata i.e. search by team label, version label. # snippetai-for-your-api-documentation Source: https://speakeasy.com/blog/snippetai-for-your-api-documentation import { ReactPlayer } from "@/lib/mdx/components"; Helping developers understand how to use your API is a critical part of API adoption. Today, we're excited to announce SnippetAI - an AI-powered code snippet generator that integrates directly into your API documentation. SnippetAI enables your users to ask natural language questions about your API and instantly receive contextualized code examples that leverage your actual SDKs, dramatically reducing integration time and improving the developer experience.
## How it works SnippetAI adds an intelligent assistant to your documentation that specializes in generating code snippets for your API. Users can: 1. Click the "Generate Code Example" button or use keyboard shortcut (⌘+S / Ctrl+S) 2. Ask a natural language question like "How do I create a new user?" 3. Receive contextually relevant code snippets that use your actual SDK implementation SnippetAI analyzes your OpenAPI specification and SDK code samples to generate snippets that match your real implementation, ensuring developers get accurate, working examples every time. ## Key benefits - **Accelerated integration**: Developers can quickly generate working code samples for your API without having to piece together documentation - **Improved Developer Experience**: Natural language interface removes friction when exploring your API's capabilities - **Higher adoption rates**: By making your API easier to implement, SnippetAI helps drive increased API usage and adoption - **Lower support burden**: Comprehensive, accurate code examples reduce the need for basic integration support - **Easy integration**: Add to your documentation site with just a few lines of code as a web component or React component ## Getting started To add SnippetAI to your documentation: 1. Enable the SnippetAI add-on in your Speakeasy dashboard under the Docs tab 2. Generate a publishing token for your target SDK 3. Integrate SnippetAI into your documentation site using one of three methods: - React component - Web Component (ES Module) - Web Component (UMD script) The integration process takes just minutes, and you can customize the appearance to match your documentation's look and feel. For detailed integration instructions, see our [SnippetAI documentation](/docs/sdk-docs/snippet-ai/overview). # speakeasy-branding-process Source: https://speakeasy.com/blog/speakeasy-branding-process _All the branding work for Speakeasy was done in partnership with our design partners,_ [_Catalog_](https://www.trycatalog.com/). Last week we rolled out some new branding for the company, replacing the hand-rolled branding which had previously been cobbled together by a team in which two of the three members were colorblind; it represents a massive improvement. How companies arrive at their branding is typically opaque to those outside the company (and often to some people within it!). Prior to this exercise, nobody on our team had participated in a proper 0-1 branding exercise. Before we started, we weren't able to identify many useful resources to help shape our expectations of what would be involved. That's why we wanted to share our own process; hopefully it gives people a window-in and provides the reference point we wished we'd had. ## What we were looking for Our design needs had three different aspects to it: Look & feel (color scheme, font, design elements, etc), logo, and mascot. The need for look & feel, and a logo are pretty self explanatory, but the desire for a mascot does require a brief explanation. We want Speakeasy to make API development a joyful activity. We believed that adding a mascot to serve as a companion for the journey through the product was something that could help bring delight to an otherwise utilitarian task. This is perhaps controversial, those old enough will still remember Clippy's incessant and obtrusive bouncing. But we think that, used sparingly and appropriately, a mascot can help bring personality to a user's interactions with a new tool. ## Step 1: Defining our values for the design team Before the designers started, we needed to articulate what values were most important to our company, so they could be central to the design process. During a couple of 1-hr sessions we focused on what is most important to our users, and therefore most important to us. We're building the developer-first [API platform](/post/why-an-api-platform-is-important/); we make it easy for developers to ship quality APIs to their customers with confidence. For developers, confidence in tooling comes from **transparency,** and **reliability**. No snake oil, black boxes, or downtime. Our platform is **honest** with our users, and **consistent** in its execution. As previously mentioned, we also believe that dev-first means building tools that are joyful for developers to use. Too often, developer tooling means a product with a densely-technical website, a cryptic set of instructions, and no UX to speak of. Whatever we build, we want it to be **approachable** for users, and give them a **direct** path to unlocking value. These words: transparency, reliability, honest, consistent, approachable, direct were the ones we handed to our designers after our internal discussions. We then turned them loose to see what they could come up with. ## Iteration 1: Casting a wide net ![First variations of different web pages with different colors and designs for Speakeasy.](/assets/blog/speakeasy-branding-process/speakeasy-branding-process-image-01.png) ![Second variations of different web pages with different colors and designs for Speakeasy.](/assets/blog/speakeasy-branding-process/speakeasy-branding-process-image-02.png) ![Third variations of different web pages with different colors and designs for Speakeasy.](/assets/blog/speakeasy-branding-process/speakeasy-branding-process-image-03.png) ![Fourth variations of different web pages with different colors and designs for Speakeasy.](/assets/blog/speakeasy-branding-process/speakeasy-branding-process-image-04.png) Our designers came back to us with four very different design styles for us to look through and give feedback on. The whole team individually recorded thoughts, before we collectively discussed to provide condensed feedback. The main takeaway that emerged from the first iteration was that it was tricky to balance approachability with reliability. How do you project openness and friendliness without appearing too flippant? Designs #1 and #4, we thought both did a good job of projecting approachability, but struggled to project reliability. Meanwhile, Design #3 did the best job at projecting reliability, but wasn't as friendly or approachable. However, we felt sure that our future look & feel lay somewhere in the intersection of the three. In terms of logos, there was a strong preference for the logo designs in #1 and #3. We did not want to have our mascot also serve as our logo. We felt that doing so would make them a static favicon, thus robbing them of the opportunity to have a personality. For iteration #2, we asked our designers to further refine the look & feel of the three options we had identified as having promise, with an eye towards cohesively integrating approachability and reliability. We also wanted to get more logo options, and begin to think about how our mascot could sit comfortably within the larger branding. ## Iteration 2: Committing to a Look & Feel ![Fifth variations of different web pages with different colors and designs for Speakeasy.](/assets/blog/speakeasy-branding-process/speakeasy-branding-process-image-05.png) ![Sixth variations of different web pages with different colors and designs for Speakeasy.](/assets/blog/speakeasy-branding-process/speakeasy-branding-process-image-06.png) ![Seventh variations of different web pages with different colors and designs for Speakeasy.](/assets/blog/speakeasy-branding-process/speakeasy-branding-process-image-07.png) From the options we received, we were determined to select a single option to move forward with. We were therefore at a critical point in the branding cycle. We decided to get opinions from the developer community. We created a feedback survey which we sent out to all the developers who were signed up to trial our product to see what they thought. This feedback exercise was a great example of why it's important to get quantitative and qualitative feedback. We asked people to score each of the designs, then rank the three, and finally provide written commentary about each design: how would they describe each design? What words came to mind? Designs #1 (two cans of red bull) and #2 (Easy as A,P,I) scored significantly higher than design #3 (Smart and light at heart) with an edge going to design #2. However, when we read through the responses, there was a much clearer alignment between design #1 and our team values. Users responded with words like, “clean”, “reliable”, “to the point”, “approachable”. As for #2, people said it was professional, but they also said it, “popped”, was “bold” and “saturated”, reminded them of “Warby Parker”, or “modern 70s design”. None of these are negative descriptions! Sentiment was overwhelmingly positive. They rated it very highly, but we felt that in some ways the design obscured the messaging we wanted to deliver. We therefore decided to go with design #1 as the basis for our look and feel. With that decided, we turned our attention to the logo and mascot. ## Iteration 2: Logo & Mascot ![First variation of logo for Speakeasy.](/assets/blog/speakeasy-branding-process/speakeasy-branding-process-image-08.png) ![Second variation of logo for Speakeasy](/assets/blog/speakeasy-branding-process/speakeasy-branding-process-image-09.png) ‍ Our user survey had also gathered feedback on the mascot and logo choices. The 16-bit pixelated mascot style had an overwhelmingly positive response, especially among the developers we interviewed (Stardew Valley has clearly left an impression on the community). However this was slightly at odds with the most popular logo; #8 was a clear favorite. We felt strongly about consistency in design style. If we had a pixelated mascot, then we wanted a pixelated logo (i.e. logos 1,2,3). However we didn't feel strongly about any of the logos presented, so we pushed our designers for some more options, specifically one with a more distinct ‘S' shape as opposed to the stacked blocks motif. We also internally had a discussion about what animal best represented the company (Mr. Diggsworth the Mole was sadly destined to be a placeholder). We discussed which animals we felt were hard workers (something we all aspire to), and very quickly focused on the natural world's best engineers: beavers, ants, and bees. After some back and forth, we landed on Beezy the Speakeasy bee. One of nature's most common and cutest builders. That they live in (api)aries was an added plus! With our look and feel & mascot both decided, we headed into another iteration to put it all together… ## Iteration 3: Bringing it all together ![Eighth variations of different web pages with different colors and designs for Speakeasy.](/assets/blog/speakeasy-branding-process/speakeasy-branding-process-image-10.png) We were overjoyed with the iteration, and felt like the branding was now in a place where we were ready to begin to see things come to life on a live webpage. And in fact the branding that we have live on our website today isn't very different from this design iteration. While building the site, a few minor alterations to the design were made after seeing what it looked like on a live screen. We swapped pink and yellow in the color hierarchy (yellow to primary accent, pink to secondary accent), and added some dark blue to the otherwise plain background. With that, we had the final design: ![Ninth variations of different web pages with different colors and designs for Speakeasy.](/assets/blog/speakeasy-branding-process/speakeasy-branding-process-image-11.png) And that was that! That is the full story of how we arrived at the branding we are using today. If you've made it this far down the article, I hope it was interesting, and that you find it useful if you ever find yourself in the midst of a design exercise. I'm sure that with time our product will expand and we will want to make updates to our design. When we do, I'll be sure to follow up with more notes on our process. # speakeasy-is-now-officially-soc-2-compliant Source: https://speakeasy.com/blog/speakeasy-is-now-officially-soc-2-compliant We're pleased to announce that Speakeasy has successfully achieved SOC 2 Type I compliance. This milestone represents our ongoing commitment to maintaining the highest standards of security and data protection for your API development workflows. ## What is SOC 2? [System and Organization Control (SOC) 2](https://www.aicpa-cima.com/topic/audit-assurance/audit-and-assurance-greater-than-soc-2), developed by the American Institute of Certified Public Accountants (AICPA), is a framework that evaluates how organizations secure and protect customer data. A Type I report specifically assesses the design of security controls at a specific point in time, ensuring they meet stringent trust service criteria. ## Our SOC 2 Audit Details - Our SOC 2 Type I audit was conducted by Sensiba LLP - The audit's reporting date is February 28, 2025 - We received a "clean" opinion with zero exceptions noted ## Why This Matters for Your API Development This certification validates that Speakeasy maintains robust security practices around our infrastructure, giving you confidence that your data and workflows are protected by independently verified security measures. As we continue enhancing our platform's capabilities, our commitment to security remains unwavering. Our goal is to let you focus on building great APIs while knowing your sensitive information is well-protected. To request a copy of our SOC 2 Type I report, please [contact us](https://www.speakeasy.com/contact). # speakeasy-vs-apimatic Source: https://speakeasy.com/blog/speakeasy-vs-apimatic import { Table } from "@/mdx/components"; At Speakeasy, we create [idiomatic SDKs](/docs/sdk-design/intro) in the most popular languages. Our generators follow principles that ensure we create SDKs that offer the best developer experience so that you can focus on building your API, and your developer-users can focus on delighting their users. In this post, we'll compare TypeScript SDKs created by Speakeasy to those generated by APIMatic. ### SDK Generation Targets At Speakeasy, we believe it is crucial to meet your users where they are by supporting SDKs in languages your users depend on. Anyone who has had to maintain custom SDK code because a vendor doesn't support their tech stack knows how frustrating this can be. This table shows the current, as of September 2024, languages and platforms targeted by Speakeasy and APIMatic. These lists will change over time, so check the official documentation for the latest language support.
We're always open to expanding our language support, but would only ever do this if we have the in-house experience to create idiomatic, best-in-class SDKs for a given language. [Let us know](/roadmap) if you would like to suggest a language or platform to support. ### SDK Features The table below compares the current SDK features offered by Speakeasy and APIMatic as of September 2024. Both Speakeasy and APIMatic are under active development, so these features may change over time.
APIMatic lacks advanced SDK customization features, and we couldn't find any code or documentation related to pagination. These are features Speakeasy's users rely on. ### Platform Features Speakeasy's primary interface is an open-source, full-featured, and portable CLI. Developers use our CLI to experiment and iterate locally and to customize their CI/CD workflows. APIMatic's CLI depends on Node.js and several packages. This makes it much less portable. In testing, we also found that it does not generate SDKs as reliably as the APIMatic web interface.
### Enterprise Support Both Speakeasy and APIMatic offer support for Enterprise customers. This includes features like concierge onboarding, private Slack channels, and enterprise SLAs.
### Pricing Speakeasy offers a free plan, while APIMatic offers a limited free trial.
Speakeasy's free plan is more generous than both Lite plans offered by APIMatic. ## Speakeasy vs APIMatic Technical Walkthrough ### Dependencies and SBOM (Software Bill of Materials)
From day one, Speakeasy has prioritized efficiency, and we've kept the dependency trees for our generated SDKs as lean as possible. For example, here's the [dependency graph for the Vercel SDK](https://npmgraph.js.org/?q=%40vercel%2Fsdk), an SDK generated by Speakeasy. It has zero direct dependencies, and Zod bundled as a regular dependency. ![Vercel dependency graph](/assets/blog/speakeasy-vs-apimatic/vercel.png) By contrast, here's the [dependency graph for the Maxio SDK](https://npmgraph.js.org/?q=@maxio-com/advanced-billing-sdk), an SDK generated by APIMatic. It has many dependencies, which in turn have transitive dependencies, leading to a much more bloated SDK that is harder to maintain. ![Maxio dependency graph](/assets/blog/speakeasy-vs-apimatic/maxio-dependency-graph.png) Having more dependencies isn't only bad in terms of efficiency. Many libraries single or no active maintainer. Many dependencies also have unaddressed critical vulnerabilities (CVs), leaving the upstream SDK vulnerable as well ([the Maxio SDK has 4 of these](https://socket.dev/npm/package/@maxio-com/advanced-billing-sdk/alerts/6.1.0?tab=dependencies)). ### Bundle size
Dependencies aren't just a security risk. They also bloat the size of your SDK. Here's a comparison of the bundle sizes for libraries generated from the same OpenAPI spec by Speakeasy and APIMatic: ![Speakeasy Bundle Size](/assets/blog/speakeasy-vs-apimatic/speakeasy-bundle-size.png) ![APIMatic Bundle Size](/assets/blog/speakeasy-vs-apimatic/apimatic-bundle-size.png) That bundle size is a problem if you have customers that need to integrate with your API in runtimes where performance is critical (browser and on the edge). ### Type safety Speakeasy creates SDKs that are type-safe from development to production. As our CEO recently wrote, [Type Safe is better than Type Faith](/post/type-safe-vs-type-faith). Speakeasy uses [Zod](https://zod.dev/) to validate data at runtime. Data sent to the server and data received from the server are validated against Zod definitions in the client. This provides safer runtime code execution and helps developers who use your SDK to provide early feedback about data entered by their end users. Furthermore, trusting data validation on the client side allows developers more confidence to build [optimistic UIs](https://medium.com/distant-horizons/using-optimistic-ui-to-delight-your-users-ac819a81d59a) that update as soon as an end user enters data, greatly improving end users' perception of your API's speed. APIMatic will only be validated on the server. This means that the error will only be caught from the client's perspective _after_ the data is sent to the server, and the server responds with an error message. As a result, developers using the SDK generated by APIMatic may need to write additional client-side validation code to catch these errors before they are sent to the server. ### Discriminated Unions Our OpenAPI document includes a `Book` component with a `category` field that can be one of three values: `Programming`, `Fantasy`, or `SciFi`. This allows us to type the `Book` component in requests and responses as specific book types, such as `ProgrammingBook`, `FantasyBook`, and `SciFiBook`. OpenAPI supports discriminated unions using the `discriminator` field in the schema. Here's an example of a response that returns an array of books of different types: ```yaml filename="openapi.yaml" schema: type: array items: oneOf: - $ref: "#/components/schemas/ProgrammingBook" - $ref: "#/components/schemas/FantasyBook" - $ref: "#/components/schemas/SciFiBook" discriminator: propertyName: category mapping: Programming: "#/components/schemas/ProgrammingBook" Fantasy: "#/components/schemas/FantasyBook" Sci-fi: "#/components/schemas/SciFiBook" ``` Let's see how the SDKs handle this. Speakeasy generates TypeScript types for each book type, and uses a discriminated union to handle the different book types. This enables developers to use the correct type when working with books of different categories. This pattern could just as easily apply to payment methods or delivery options. The example below shows how Speakeasy defines the `ProgrammingBook` type. It also generates types for `FantasyBook` and `SciFiBook`. In this example, you'll notice that the `category` field is optional in the `ProgrammingBook` type, but is enforced by Zod validation in the SDK. ```typescript filename="speakeasy/books.ts" mark=16,29,50,61 /* * Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT. */ import { Author, Author$ } from "./author"; import * as z from "zod"; export type ProgrammingBook = { id?: number | undefined; title: string; description: string; /** * Price in USD cents */ price: number; category?: "Programming" | undefined; author: Author; coverImage?: string | undefined; }; /** @internal */ export namespace ProgrammingBook$ { export const inboundSchema: z.ZodType = z .object({ id: z.number().int().optional(), title: z.string(), description: z.string(), price: z.number().int(), category: z.literal("Programming").optional(), author: Author$.inboundSchema, cover_image: z.string().optional(), }) .transform((v) => { return { ...(v.id === undefined ? null : { id: v.id }), title: v.title, description: v.description, price: v.price, ...(v.category === undefined ? null : { category: v.category }), author: v.author, ...(v.cover_image === undefined ? null : { coverImage: v.cover_image }), }; }); export type Outbound = { id?: number | undefined; title: string; description: string; price: number; category: "Programming"; author: Author$.Outbound; cover_image?: string | undefined; }; export const outboundSchema: z.ZodType = z .object({ id: z.number().int().optional(), title: z.string(), description: z.string(), price: z.number().int(), category: z.literal("Programming").default("Programming" as const), author: Author$.outboundSchema, coverImage: z.string().optional(), }) .transform((v) => { return { ...(v.id === undefined ? null : { id: v.id }), title: v.title, description: v.description, price: v.price, category: v.category, author: v.author, ...(v.coverImage === undefined ? null : { cover_image: v.coverImage }), }; }); } ``` We can see how Speakeasy generates SDK code to handle the different book types in the response for the `getgetAllBooks` operation: ```typescript filename="speakeasy/getallbooks.ts" /* * Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT. */ import * as components from "../components"; import * as z from "zod"; export type ResponseBody = | (components.ProgrammingBook & { category: "Programming" }) | (components.FantasyBook & { category: "Fantasy" }) | (components.SciFiBook & { category: "Sci-fi" }); export type GetAllBooksResponse = { httpMeta: components.HTTPMetadata; /** * A list of books */ responseBodies?: | Array< | (components.ProgrammingBook & { category: "Programming" }) | (components.FantasyBook & { category: "Fantasy" }) | (components.SciFiBook & { category: "Sci-fi" }) > | undefined; }; // ... ``` Note how the array elements in `responseBodies` are typed according to the book category. This may seem like a trivial example, but it illustrates how Speakeasy generates types that are more specific and easier to work with than the types generated by APIMatic. This could, for instance, help developers correctly handle different book types in their applications. APIMatic does not generate types for discriminated unions, and developers must manually handle the different book types in the response. Here is the equivalent type definition generated by APIMatic: ```typescript filename="apimatic/programmingBook.ts" /** * Bookstore APILib * * This file was automatically generated by APIMATIC v3.0 ( https://www.apimatic.io ). */ // ... import { CategoryEnum, categoryEnumSchema } from './categoryEnum'; // ... export interface ProgrammingBook { id?: number; title: string; description: string; /** Price in USD cents */ price: number; category: CategoryEnum; author: Author2; coverImage?: string; } // ... ``` Following the `CategoryEnum` import: ```typescript filename="apimatic/categoryEnum.ts" /** * Bookstore APILib * * This file was automatically generated by APIMATIC v3.0 ( https://www.apimatic.io ). */ // ... /** * Enum for CategoryEnum */ export enum CategoryEnum { Scifi = 'Sci-fi', Fantasy = 'Fantasy', Programming = 'Programming', } // ... ``` Discriminating between different book types in the response is left to users. ## Speakeasy Compared to Open-Source Generators If you are interested in seeing how Speakeasy stacks up against other SDK generation tools, check out our [post](/post/compare-speakeasy-open-source). # will require GitHub login in browser Source: https://speakeasy.com/blog/speakeasy-vs-fern import { Callout, Table } from "@/mdx/components"; This comparison of Speakeasy and Fern is based on a snapshot of two developing companies as of January 2025. If you think we need to update this post, please let us know! [Speakeasy](https://www.speakeasy.com/) and [Fern](https://buildwithfern.com/) both offer free and paid services that API developers use to create SDKs (client libraries) and automate SDKs' publication to package managers, but how do they differ? Here's the short answer: 1. **Fern** is an SDK generation tool designed for the Fern domain-specific language (DSL). It creates SDKs in seven languages and API reference documentation. 2. **Speakeasy** is a complete platform for building and exposing enterprise APIs. It is OpenAPI-native and supports SDK generation in ten languages, as well as Terraform providers and documentation. ## How is Speakeasy different? ### Speakeasy is everything you need in one We've built a platform that does more than merely generate SDKs. You could use Fern for SDKs, Stoplight for documentation, Spectral for linting, and handroll your Terraform provider, or you could use Speakeasy to do it all. One platform, one team, all your API needs handled. ### OpenAPI-native vs OpenAPI-compatible Speakeasy is designed to be **OpenAPI-native**. We don't believe the world needs another standard for describing APIs. OpenAPI has its flaws, but it's the established standard, and we're committed to making it better. That means that Speakeasy is interoperable with the rest of the API tooling ecosystem. Mix and match us with your other favorite tools, and we'll play nice. Fern is built on top of a [DSL (domain-specific language)](https://buildwithfern.com/learn/api-definition/fern/overview), with **optional** support for OpenAPI. This makes Fern **OpenAPI-compatible**, meaning your OpenAPI document is no longer the single source of truth for your API. ### Engineering velocity and maturity Fern's initial GitHub commit was in [April 2022](https://github.com/fern-api/fern/commit/322908f557ee94882ed64f265993ed53ae002198), and the tool has expanded its language support to seven languages since then. By comparison, [Speakeasy's first commit was in September 2022](https://github.com/speakeasy-api/speakeasy/commit/cb126d57ba7ad2ed9c1445a396beb5b48714da80), and the platform has released support for ten languages in a shorter period. The Speakeasy platform is also broader, with support for additional generation features not supported by Fern, like [React Hooks](/post/release-react-hooks) and [Terraform providers](/post/release-terraform-v2). ### Speakeasy SDKs work where you need them Speakeasy SDKs are designed to work in any environment. Speakeasy supports the latest versions of the languages it targets, and we're committed to staying up to date with new releases. Our TypeScript SDKs can be bundled for the browser and many other JavaScript environments, while Fern's are Node.js-only. Speakeasy gets high-quality products in the hands of our users fast. ## Comparing Speakeasy and Fern ### SDK generation Everyone has that one odd language that is critically important to their business and seemingly to nobody else's. That's why we're committed to supporting the long tail. We've made a dent, but we've got further to go. Is there a language you need that we don't support? [Join our Slack community](https://go.speakeasy.com/slack) to let us know.
### SDK features Fern and Speakeasy SDKs differ in two key areas of feature support: 1. Fern lacks native support for some of the more advanced enterprise features supported by Speakeasy. Features like pagination and OAuth are left up to the customer to implement with custom code. 2. Fern offers customizations to the names used in the SDK but not to the fundamental structure of the SDK. In addition to names, Speakeasy allows you to customize details like the directory structure and how parameters are passed into functions.
### Platform features The primary differences between the platforms are: 1. Fern is solely focused on the generation of artifacts. Speakeasy has a deeper platform that supports the management of API creation through CLI validation. 2. Speakeasy offers a web interface for managing and monitoring the creation of your SDKs.
⚠️ Fern claims CI/CD support for SDKs on its paid plan, but this feature is not mentioned in the documentation. ### Dependencies and SBOM (Software Bill of Materials) From day one, Speakeasy has prioritized efficiency, and we've kept the dependency trees for our generated SDKs as lean as possible. For example, here's the [dependency graph for the Vercel SDK](https://npmgraph.js.org/?q=%40vercel%2Fsdk), an SDK generated by Speakeasy. It has zero direct dependencies, and Zod bundled as a regular dependency. ![Vercel dependency graph](/assets/blog/speakeasy-vs-fern/vercel.png) By contrast, here's the [dependency graph for the ElevenLabs SDK](https://npmgraph.js.org/?q=elevenlabs), an SDK generated by Fern. It has many dependencies, which in turn have transitive dependencies, leading to a much more bloated SDK that is harder to maintain. ![ElevenLabs dependency graph](/assets/blog/speakeasy-vs-fern/elevenlabs.png) Having more dependencies isn't only bad in terms of efficiency. Many libraries might have only a single maintainer (the ElevenLabs SDK has 36 of these). This means any of these libraries could become unmaintained without warning. Similarly, many dependencies might have unaddressed critical vulnerabilities (CVs), leaving the upstream SDK vulnerable as well. ### Enterprise support Speakeasy sets up tracking on all customer repositories and will proactively triage any issues that arise.
### Pricing The biggest difference between the two pricing models is the starter plan. Speakeasy offers one free SDK with up to 250 endpoints, while Fern's starter plan is paid.
## Fern and Speakeasy walkthrough Let's walk through generating an SDK with both Fern and Speakeasy. This is well explained in the documentation, so we'll keep it brief. Both services support Linux, macOS, and Windows, and run in Docker. Some of the examples below are from the [Speakeasy Bar Starter SDK](https://github.com/speakeasy-sdks/template-speakeasy-bar/). ## Creating SDKs ### Fern quickstart Follow the [Fern quickstart](https://buildwithfern.com/learn/sdks/getting-started/generate-your-first-sdk). In the folder containing the `openapi.yaml` file, open a terminal and use Node.js with npm: ```bash npm install -g fern-api fern init --openapi ./openapi.yaml; fern generate ``` - `init` creates a `fern` folder containing a copy of the OpenAPI document and some configuration files. - `generate` creates SDKs in the folder `../generated`. You can change the output folder by editing `generators.yaml`. We used the following file to create all four languages: ```yaml default-group: local groups: local: generators: - name: fernapi/fern-typescript-node-sdk version: 0.7.2 output: location: local-file-system path: ./generated/typescript config: outputSourceFiles: true # output .ts instead of .js with definitions files - name: fernapi/fern-python-sdk version: 0.7.2 output: location: local-file-system path: ./generated/python - name: fernapi/fern-java-sdk version: 0.5.15 output: location: local-file-system path: ../generated/java - name: fernapi/fern-go-sdk version: 0.9.2 output: location: local-file-system path: ../generated/go - name: fernapi/fern-postman version: 0.0.45 output: location: local-file-system path: ./generated/postman ``` Fern can also generate documentation: - `init --docs` creates a `docs.yml` configuration file. - `generate --docs;` creates documentation at the URL specified in the configuration file. ### Speakeasy quickstart Follow the [Speakeasy quickstart](https://speakeasy.com/docs/speakeasy-cli/getting-started). The Speakeasy CLI is a single executable file [built with Go](https://github.com/speakeasy-api/speakeasy). ```bash brew install speakeasy-api/tap/speakeasy speakeasy quickstart ``` - Speakeasy handles authentication with a secret key in an environment variable. You can get the secret key on the Speakeasy website. - Running the Speakeasy quickstart launches an interactive mode that will guide you through generating an SDK. ## Comparing TypeScript SDK generation with Fern and Speakeasy Comparing the output of Fern and Speakeasy for all four SDK languages Fern supports would be too long for this article. We'll focus on TypeScript (JavaScript). ### SDK structure Below is the Fern folder structure. ```bash ├── Client.d.ts ├── Client.js ├── api │ ├── errors │ │ ├── BadRequestError.d.ts │ │ ├── BadRequestError.js │ │ ├── UnauthorizedError.d.ts │ │ ├── UnauthorizedError.js │ │ ├── index.d.ts │ │ └── index.js │ ├── index.d.ts │ ├── index.js │ ├── resources │ │ ├── authentication │ │ │ ├── client │ │ │ │ ├── Client.d.ts │ │ │ │ ├── Client.js │ │ │ │ ├── index.d.ts │ │ │ │ ├── index.js │ │ │ │ └── requests │ │ │ │ ├── LoginRequest.d.ts │ │ │ │ ├── LoginRequest.js │ │ │ │ ├── index.d.ts │ │ │ │ └── index.js │ │ │ ├── index.d.ts │ │ │ ├── index.js │ │ │ └── types │ │ │ ├── LoginRequestType.d.ts │ │ │ ├── LoginRequestType.js │ │ │ ├── LoginResponse.d.ts │ │ │ ├── LoginResponse.js │ │ │ ├── index.d.ts │ │ │ └── index.js │ │ ├── config │ │ │ ├── client │ │ │ │ ├── Client.d.ts │ │ │ │ ├── Client.js │ │ │ │ ├── index.d.ts │ │ │ │ └── index.js │ │ │ ├── index.d.ts │ │ │ ├── index.js │ │ │ └── types │ │ │ ├── SubscribeToWebhooksRequestItem.d.ts │ │ │ ├── SubscribeToWebhooksRequestItem.js │ │ │ ├── index.d.ts │ │ │ └── index.js │ │ ├── drinks │ │ │ ├── client │ │ │ │ ├── Client.d.ts │ │ │ │ ├── Client.js │ │ │ │ ├── index.d.ts │ │ │ │ ├── index.js │ │ │ │ └── requests │ │ │ │ ├── ListDrinksRequest.d.ts │ │ │ │ ├── ListDrinksRequest.js │ │ │ │ ├── index.d.ts │ │ │ │ └── index.js │ │ │ ├── index.d.ts │ │ │ └── index.js │ │ ├── index.d.ts │ │ ├── index.js │ │ ├── ingredients │ │ │ ├── client │ │ │ │ ├── Client.d.ts │ │ │ │ ├── Client.js │ │ │ │ ├── index.d.ts │ │ │ │ ├── index.js │ │ │ │ └── requests │ │ │ │ ├── ListIngredientsRequest.d.ts │ │ │ │ ├── ListIngredientsRequest.js │ │ │ │ ├── index.d.ts │ │ │ │ └── index.js │ │ │ ├── index.d.ts │ │ │ └── index.js │ │ └── orders │ │ ├── client │ │ │ ├── Client.d.ts │ │ │ ├── Client.js │ │ │ ├── index.d.ts │ │ │ ├── index.js │ │ │ └── requests │ │ │ ├── CreateOrderRequest.d.ts │ │ │ ├── CreateOrderRequest.js │ │ │ ├── index.d.ts │ │ │ └── index.js │ │ ├── index.d.ts │ │ └── index.js │ └── types │ ├── ApiError.d.ts │ ├── ApiError.js │ ├── BadRequest.d.ts │ ├── BadRequest.js │ ├── Drink.d.ts │ ├── Drink.js │ ├── DrinkType.d.ts │ ├── DrinkType.js │ ├── Error_.d.ts │ ├── Error_.js │ ├── Ingredient.d.ts │ ├── Ingredient.js │ ├── IngredientType.d.ts │ ├── IngredientType.js │ ├── Order.d.ts │ ├── Order.js │ ├── OrderStatus.d.ts │ ├── OrderStatus.js │ ├── OrderType.d.ts │ ├── OrderType.js │ ├── index.d.ts │ └── index.js ├── core │ ├── fetcher │ │ ├── APIResponse.d.ts │ │ ├── APIResponse.js │ │ ├── Fetcher.d.ts │ │ ├── Fetcher.js │ │ ├── Supplier.d.ts │ │ ├── Supplier.js │ │ ├── createRequestUrl.d.ts │ │ ├── createRequestUrl.js │ │ ├── getFetchFn.d.ts │ │ ├── getFetchFn.js │ │ ├── getHeader.d.ts │ │ ├── getHeader.js │ │ ├── getRequestBody.d.ts │ │ ├── getRequestBody.js │ │ ├── getResponseBody.d.ts │ │ ├── getResponseBody.js │ │ ├── index.d.ts │ │ ├── index.js │ │ ├── makeRequest.d.ts │ │ ├── makeRequest.js │ │ ├── requestWithRetries.d.ts │ │ ├── requestWithRetries.js │ │ ├── signals.d.ts │ │ ├── signals.js │ │ └── stream-wrappers │ │ ├── Node18UniversalStreamWrapper.d.ts │ │ ├── Node18UniversalStreamWrapper.js │ │ ├── NodePre18StreamWrapper.d.ts │ │ ├── NodePre18StreamWrapper.js │ │ ├── UndiciStreamWrapper.d.ts │ │ ├── UndiciStreamWrapper.js │ │ ├── chooseStreamWrapper.d.ts │ │ └── chooseStreamWrapper.js │ ├── index.d.ts │ ├── index.js │ ├── runtime │ │ ├── index.d.ts │ │ ├── index.js │ │ ├── runtime.d.ts │ │ └── runtime.js │ └── schemas │ ├── Schema.d.ts │ ├── Schema.js │ ├── builders │ │ ├── date │ │ │ ├── date.d.ts │ │ │ ├── date.js │ │ │ ├── index.d.ts │ │ │ └── index.js │ │ ├── enum │ │ │ ├── enum.d.ts │ │ │ ├── enum.js │ │ │ ├── index.d.ts │ │ │ └── index.js │ │ ├── index.d.ts │ │ ├── index.js │ │ ├── lazy │ │ │ ├── index.d.ts │ │ │ ├── index.js │ │ │ ├── lazy.d.ts │ │ │ ├── lazy.js │ │ │ ├── lazyObject.d.ts │ │ │ └── lazyObject.js │ │ ├── list │ │ │ ├── index.d.ts │ │ │ ├── index.js │ │ │ ├── list.d.ts │ │ │ └── list.js │ │ ├── literals │ │ │ ├── booleanLiteral.d.ts │ │ │ ├── booleanLiteral.js │ │ │ ├── index.d.ts │ │ │ ├── index.js │ │ │ ├── stringLiteral.d.ts │ │ │ └── stringLiteral.js │ │ ├── object │ │ │ ├── index.d.ts │ │ │ ├── index.js │ │ │ ├── object.d.ts │ │ │ ├── object.js │ │ │ ├── objectWithoutOptionalProperties.d.ts │ │ │ ├── objectWithoutOptionalProperties.js │ │ │ ├── property.d.ts │ │ │ ├── property.js │ │ │ ├── types.d.ts │ │ │ └── types.js │ │ ├── object-like │ │ │ ├── getObjectLikeUtils.d.ts │ │ │ ├── getObjectLikeUtils.js │ │ │ ├── index.d.ts │ │ │ ├── index.js │ │ │ ├── types.d.ts │ │ │ └── types.js │ │ ├── primitives │ │ │ ├── any.d.ts │ │ │ ├── any.js │ │ │ ├── boolean.d.ts │ │ │ ├── boolean.js │ │ │ ├── index.d.ts │ │ │ ├── index.js │ │ │ ├── number.d.ts │ │ │ ├── number.js │ │ │ ├── string.d.ts │ │ │ ├── string.js │ │ │ ├── unknown.d.ts │ │ │ └── unknown.js │ │ ├── record │ │ │ ├── index.d.ts │ │ │ ├── index.js │ │ │ ├── record.d.ts │ │ │ ├── record.js │ │ │ ├── types.d.ts │ │ │ └── types.js │ │ ├── schema-utils │ │ │ ├── JsonError.d.ts │ │ │ ├── JsonError.js │ │ │ ├── ParseError.d.ts │ │ │ ├── ParseError.js │ │ │ ├── getSchemaUtils.d.ts │ │ │ ├── getSchemaUtils.js │ │ │ ├── index.d.ts │ │ │ ├── index.js │ │ │ ├── stringifyValidationErrors.d.ts │ │ │ └── stringifyValidationErrors.js │ │ ├── set │ │ │ ├── index.d.ts │ │ │ ├── index.js │ │ │ ├── set.d.ts │ │ │ └── set.js │ │ ├── undiscriminated-union │ │ │ ├── index.d.ts │ │ │ ├── index.js │ │ │ ├── types.d.ts │ │ │ ├── types.js │ │ │ ├── undiscriminatedUnion.d.ts │ │ │ └── undiscriminatedUnion.js │ │ └── union │ │ ├── discriminant.d.ts │ │ ├── discriminant.js │ │ ├── index.d.ts │ │ ├── index.js │ │ ├── types.d.ts │ │ ├── types.js │ │ ├── union.d.ts │ │ └── union.js │ ├── index.d.ts │ ├── index.js │ └── utils │ ├── MaybePromise.d.ts │ ├── MaybePromise.js │ ├── addQuestionMarksToNullableProperties.d.ts │ ├── addQuestionMarksToNullableProperties.js │ ├── createIdentitySchemaCreator.d.ts │ ├── createIdentitySchemaCreator.js │ ├── entries.d.ts │ ├── entries.js │ ├── filterObject.d.ts │ ├── filterObject.js │ ├── getErrorMessageForIncorrectType.d.ts │ ├── getErrorMessageForIncorrectType.js │ ├── isPlainObject.d.ts │ ├── isPlainObject.js │ ├── keys.d.ts │ ├── keys.js │ ├── maybeSkipValidation.d.ts │ ├── maybeSkipValidation.js │ ├── partition.d.ts │ └── partition.js ├── environments.d.ts ├── environments.js ├── errors │ ├── NdimaresApiError.d.ts │ ├── NdimaresApiError.js │ ├── NdimaresApiTimeoutError.d.ts │ ├── NdimaresApiTimeoutError.js │ ├── index.d.ts │ └── index.js ├── index.d.ts ├── index.js └── serialization ├── index.d.ts ├── index.js ├── resources │ ├── authentication │ │ ├── client │ │ │ ├── index.d.ts │ │ │ ├── index.js │ │ │ └── requests │ │ │ ├── LoginRequest.d.ts │ │ │ ├── LoginRequest.js │ │ │ ├── index.d.ts │ │ │ └── index.js │ │ ├── index.d.ts │ │ ├── index.js │ │ └── types │ │ ├── LoginRequestType.d.ts │ │ ├── LoginRequestType.js │ │ ├── LoginResponse.d.ts │ │ ├── LoginResponse.js │ │ ├── index.d.ts │ │ └── index.js │ ├── config │ │ ├── client │ │ │ ├── index.d.ts │ │ │ ├── index.js │ │ │ ├── subscribeToWebhooks.d.ts │ │ │ └── subscribeToWebhooks.js │ │ ├── index.d.ts │ │ ├── index.js │ │ └── types │ │ ├── SubscribeToWebhooksRequestItem.d.ts │ │ ├── SubscribeToWebhooksRequestItem.js │ │ ├── index.d.ts │ │ └── index.js │ ├── drinks │ │ ├── client │ │ │ ├── index.d.ts │ │ │ ├── index.js │ │ │ ├── listDrinks.d.ts │ │ │ └── listDrinks.js │ │ ├── index.d.ts │ │ └── index.js │ ├── index.d.ts │ ├── index.js │ ├── ingredients │ │ ├── client │ │ │ ├── index.d.ts │ │ │ ├── index.js │ │ │ ├── listIngredients.d.ts │ │ │ └── listIngredients.js │ │ ├── index.d.ts │ │ └── index.js │ └── orders │ ├── client │ │ ├── createOrder.d.ts │ │ ├── createOrder.js │ │ ├── index.d.ts │ │ └── index.js │ ├── index.d.ts │ └── index.js └── types ├── ApiError.d.ts ├── ApiError.js ├── BadRequest.d.ts ├── BadRequest.js ├── Drink.d.ts ├── Drink.js ├── DrinkType.d.ts ├── DrinkType.js ├── Error_.d.ts ├── Error_.js ├── Ingredient.d.ts ├── Ingredient.js ├── IngredientType.d.ts ├── IngredientType.js ├── Order.d.ts ├── Order.js ├── OrderStatus.d.ts ├── OrderStatus.js ├── OrderType.d.ts ├── OrderType.js ├── index.d.ts └── index.js ``` Below is the Speakeasy folder structure. ```bash ├── CONTRIBUTING.md ├── FUNCTIONS.md ├── README.md ├── RUNTIMES.md ├── USAGE.md ├── docs │ ├── lib │ │ └── utils │ │ └── retryconfig.md │ ├── models │ │ ├── callbacks │ │ │ └── createorderorderupdaterequestbody.md │ │ ├── components │ │ │ ├── drink.md │ │ │ ├── drinktype.md │ │ │ ├── errort.md │ │ │ ├── ingredient.md │ │ │ ├── ingredienttype.md │ │ │ ├── order.md │ │ │ ├── ordertype.md │ │ │ ├── security.md │ │ │ └── status.md │ │ ├── errors │ │ │ ├── apierror.md │ │ │ └── badrequest.md │ │ ├── operations │ │ │ ├── createorderrequest.md │ │ │ ├── createorderresponse.md │ │ │ ├── getdrinkrequest.md │ │ │ ├── getdrinkresponse.md │ │ │ ├── listdrinksrequest.md │ │ │ ├── listdrinksresponse.md │ │ │ ├── listdrinkssecurity.md │ │ │ ├── listingredientsrequest.md │ │ │ ├── listingredientsresponse.md │ │ │ ├── loginrequestbody.md │ │ │ ├── loginresponse.md │ │ │ ├── loginresponsebody.md │ │ │ ├── loginsecurity.md │ │ │ ├── requestbody.md │ │ │ ├── type.md │ │ │ └── webhook.md │ │ └── webhooks │ │ └── stockupdaterequestbody.md │ └── sdks │ ├── authentication │ │ └── README.md │ ├── config │ │ └── README.md │ ├── drinks │ │ └── README.md │ ├── ingredients │ │ └── README.md │ ├── orders │ │ └── README.md │ └── sdk │ └── README.md ├── jsr.json ├── package.json ├── src │ ├── core.ts │ ├── funcs │ │ ├── authenticationLogin.ts │ │ ├── configSubscribeToWebhooks.ts │ │ ├── drinksGetDrink.ts │ │ ├── drinksListDrinks.ts │ │ ├── ingredientsListIngredients.ts │ │ └── ordersCreateOrder.ts │ ├── hooks │ │ ├── hooks.ts │ │ ├── index.ts │ │ ├── registration.ts │ │ └── types.ts │ ├── index.ts │ ├── lib │ │ ├── base64.ts │ │ ├── config.ts │ │ ├── dlv.ts │ │ ├── encodings.ts │ │ ├── files.ts │ │ ├── http.ts │ │ ├── is-plain-object.ts │ │ ├── logger.ts │ │ ├── matchers.ts │ │ ├── primitives.ts │ │ ├── retries.ts │ │ ├── schemas.ts │ │ ├── sdks.ts │ │ ├── security.ts │ │ └── url.ts │ ├── models │ │ ├── callbacks │ │ │ ├── createorder.ts │ │ │ └── index.ts │ │ ├── components │ │ │ ├── drink.ts │ │ │ ├── drinkinput.ts │ │ │ ├── drinktype.ts │ │ │ ├── error.ts │ │ │ ├── index.ts │ │ │ ├── ingredient.ts │ │ │ ├── ingredientinput.ts │ │ │ ├── ingredienttype.ts │ │ │ ├── order.ts │ │ │ ├── orderinput.ts │ │ │ ├── ordertype.ts │ │ │ └── security.ts │ │ ├── errors │ │ │ ├── apierror.ts │ │ │ ├── badrequest.ts │ │ │ ├── httpclienterrors.ts │ │ │ ├── index.ts │ │ │ ├── sdkerror.ts │ │ │ └── sdkvalidationerror.ts │ │ ├── operations │ │ │ ├── createorder.ts │ │ │ ├── getdrink.ts │ │ │ ├── index.ts │ │ │ ├── listdrinks.ts │ │ │ ├── listingredients.ts │ │ │ ├── login.ts │ │ │ └── subscribetowebhooks.ts │ │ └── webhooks │ │ ├── index.ts │ │ └── stockupdate.ts │ ├── sdk │ │ ├── authentication.ts │ │ ├── config.ts │ │ ├── drinks.ts │ │ ├── index.ts │ │ ├── ingredients.ts │ │ ├── orders.ts │ │ └── sdk.ts │ └── types │ ├── blobs.ts │ ├── constdatetime.ts │ ├── enums.ts │ ├── fp.ts │ ├── index.ts │ ├── operations.ts │ ├── rfcdate.ts │ └── streams.ts └── tsconfig.json ``` Speakeasy includes a documentation folder next to the SDK folder. Speakeasy creates a complete npm package, with a `package.json` file, that is ready to be published to the npm registry. With Fern, you have to do extra work to prepare for publishing. The structure of the SDK also has some bearing on the DevEx. To call order functions in the SDKs, you would use `api/resources/orders/Client.js` in Fern and `src/sdk/orders.ts` in Speakeasy. ### Example SDK method Let's take a look at the code for a single call, `createOrder`, in Fern and Speakeasy.
```ts filename="fern-example/createOrder.ts" /\*\* - Create an order for a drink. _/ createOrder(request, requestOptions) { var \_a; return \_\_awaiter(this, void 0, void 0, function_ () { const { callbackUrl, body: \_body } = request; const \_queryParams = new url_search_params_1.default(); if (callbackUrl != null) { \_queryParams.append("callback_url", callbackUrl); } const \_response = yield core.fetcher({ url: (0, url_join_1.default)((\_a = (yield core.Supplier.get(this.\_options.environment))) !== null && \_a !== void 0 ? \_a : environments.NdimaresApiEnvironment.Default, "order"), method: "POST", headers: { Authorization: yield this.\_getAuthorizationHeader(), "X-Fern-Language": "JavaScript", }, contentType: "application/json", queryParameters: \_queryParams, body: yield serializers.orders.createOrder.Request.jsonOrThrow(\_body, { unrecognizedObjectKeys: "strip" }), timeoutMs: (requestOptions === null || requestOptions === void 0 ? void 0 : requestOptions.timeoutInSeconds) != null ? requestOptions.timeoutInSeconds \* 1000 : 60000, }); if (\_response.ok) { return yield serializers.Order.parseOrThrow(\_response.body, { unrecognizedObjectKeys: "passthrough", allowUnrecognizedUnionMembers: true, allowUnrecognizedEnumValues: true, breadcrumbsPrefix: ["response"], }); } if (\_response.error.reason === "status-code") { throw new errors.NdimaresApiError({ statusCode: \_response.error.statusCode, body: \_response.error.body, }); } switch (\_response.error.reason) { case "non-json": throw new errors.NdimaresApiError({ statusCode: \_response.error.statusCode, body: \_response.error.rawBody, }); case "timeout": throw new errors.NdimaresApiTimeoutError(); case "unknown": throw new errors.NdimaresApiError({ message: \_response.error.errorMessage, }); } }); } ```
```ts filename="speakeasy-example/createOrder.ts" /** * Create an order. * * @remarks * Create an order for a drink. */ export async function ordersCreateOrder( client$: SDKCore, request: operations.CreateOrderRequest, options?: RequestOptions ): Promise< Result< operations.CreateOrderResponse, | errors.APIError | SDKError | SDKValidationError | UnexpectedClientError | InvalidRequestError | RequestAbortedError | RequestTimeoutError | ConnectionError > > { const input$ = request; const parsed$ = schemas$.safeParse( input$, (value$) => operations.CreateOrderRequest$outboundSchema.parse(value$), "Input validation failed" ); if (!parsed$.ok) { return parsed$; } const payload$ = parsed$.value; const body$ = encodeJSON$("body", payload$.RequestBody, { explode: true }); const path$ = pathToFunc("/order")(); const query$ = encodeFormQuery$({ callback_url: payload$.callback_url, }); const headers$ = new Headers({ "Content-Type": "application/json", Accept: "application/json", }); const security$ = await extractSecurity(client$.options$.security); const context = { operationID: "createOrder", oAuth2Scopes: [], securitySource: client$.options$.security, }; const securitySettings$ = resolveGlobalSecurity(security$); const requestRes = client$.createRequest$( context, { security: securitySettings$, method: "POST", path: path$, headers: headers$, query: query$, body: body$, timeoutMs: options?.timeoutMs || client$.options$.timeoutMs || -1, }, options ); if (!requestRes.ok) { return requestRes; } const request$ = requestRes.value; const doResult = await client$.do$(request$, { context, errorCodes: ["4XX", "5XX"], retryConfig: options?.retries || client$.options$.retryConfig, retryCodes: options?.retryCodes || ["429", "500", "502", "503", "504"], }); if (!doResult.ok) { return doResult; } const response = doResult.value; const responseFields$ = { HttpMeta: { Response: response, Request: request$ }, }; const [result$] = await m$.match< operations.CreateOrderResponse, | errors.APIError | SDKError | SDKValidationError | UnexpectedClientError | InvalidRequestError | RequestAbortedError | RequestTimeoutError | ConnectionError >( m$.json(200, operations.CreateOrderResponse$inboundSchema), m$.fail("4XX"), m$.jsonErr("5XX", errors.APIError$inboundSchema), m$.json("default", operations.CreateOrderResponse$inboundSchema) )(response, { extraFields: responseFields$ }); if (!result$.ok) { return result$; } return result$; } ```
### Type safety Both Fern and Speakeasy ensure that, if the input is incorrect, the SDK will throw an error instead of silently giving you incorrect data. Fern uses a custom data serialization validator to validate every object received by your SDK from the server. See an example of this in `api/resources/pet/client/Client.ts`, where the line `return await serializers.Pet.parseOrThrow(_response.body, {` calls into the `core/schemas/builders` code. Speakeasy uses [Zod](https://github.com/colinhacks/zod), an open-source validator, eliminating the need for custom serialization code. ### File streaming Streaming file transmission allows servers and clients to do gradual processing, which is useful for playing videos or transforming long text files. Fern [supports file streaming](https://buildwithfern.com/learn/api-definition/openapi/endpoints/sse) but with the use of a proprietary endpoint extension, `x-fern-streaming: true`. Speakeasy supports the [Streams API](https://developer.mozilla.org/en-US/docs/Web/API/Streams_API) web standard automatically. You can use code like the following to upload and download large files: ```js const fileHandle = await openAsBlob("./src/sample.txt"); const result = await sdk.upload({ file: fileHandle }); ``` ### React Hooks React Hooks simplify state and data management in React apps, enabling developers to consume APIs more efficiently. Fern does not support React Hooks natively. Developers must manually integrate SDK methods into state management tools like React Context, Redux, or TanStack Query. Speakeasy generates built-in React Hooks using [TanStack Query](https://tanstack.com/query/latest). These hooks provide features like intelligent caching, type safety, pagination, and seamless integration with modern React patterns such as SSR and Suspense. Here's an example: ```typescript filename="example/loadPosts.tsx" import { useQuery } from "@tanstack/react-query"; function Posts() { const { data, status, error } = useQuery([ "posts" // Cache key for the query ], async () => { const response = await fetch("https://jsonplaceholder.typicode.com/posts"); return response.json(); }); if (status === "loading") return

Loading posts...

; if (status === "error") return

Error: {error?.message}

; return (
    {data.map((post) => (
  • {post.title}
  • ))}
); } ``` In this example, the `useQuery` hook fetches data from an API endpoint. The cache key ensures unique identification of the query. The `status` variable provides the current state of the query: `loading`, `error`, or `success`. Depending on the query status, the component renders `loading`, `error`, or the fetched data as a list. #### Auto-pagination Speakeasy's React Hooks also enable auto-pagination, which automatically fetches more data when the user scrolls to the bottom of the page. This feature is useful for infinite scrolling in social media feeds or search results. ```typescript filename="example/PostsView.tsx" import { useInView } from "react-intersection-observer"; import { useActorAuthorFeedInfinite } from "@speakeasy-api/bluesky/react-query/actorAuthorFeed.js"; export function PostsView(props: { did: string }) { const { data, fetchNextPage, hasNextPage } = useActorAuthorFeedInfinite({ actor: props.did, }); const { ref } = useInView({ rootMargin: "50px", onChange(inView) { if (inView) { fetchNextPage(); } }, }); return (
    {data?.pages.flatMap((page) => { return page.result.feed.map((entry) => (
  • )); })}
{hasNextPage ?
: null}
); ``` Fern also supports pagination, but only offset- and cursor-based pagination, and these require additional configuration. For an in-depth look at how Speakeasy uses React Hooks, see our [official release article](https://www.speakeasy.com/post/release-react-hooks). ### Webhooks support Webhooks enable users to receive real-time updates from your API through HTTP callbacks in your SDK. Both Speakeasy and Fern generate SDKs that support webhooks and provide built-in support for webhook validation, payload parsing, and delivery. However, the way the platforms handle webhooks differs slightly. Speakeasy provides a higher-level abstraction that includes validation and event type inference, whereas Fern requires manual event handling after signature verification. We'll use an example bookstore API to demonstrate how both SDKs handle webhooks. First we'll look at the OpenAPI definition for the webhook: ```yaml openapi: 3.1.1 paths: ... x-speakeasy-webhooks: security: type: signature # a preset which signs the request body with HMAC name: x-signature # the name of the header encoding: base64 # encoding of the signature in the header algorithm: hmac-sha256 webhooks: book.created: post: requestBody: required: true content: application/json: schema: type: object properties: id: type: string title: type: string required: - id - title responses: "200": description: Book creation event received book.deleted: ... ``` Here's how you would handle the webhook using the SDKs:
```typescript filename="speakeasy-example/webhook.ts" // techbooks-speakeasy SDK created by Speakeasy import { TechBooks } from "techbooks-speakeasy"; const bookStore = new TechBooks(); async function handleWebhook(request: Request) { const secret = "my-webhook-secret"; const res = await bookStore.webhooks.validateWebhook({ request, secret }); if (res.error) { console.error("Webhook validation failed:", res.error); throw new Error("Invalid webhook signature"); } // Speakeasy provides type inference and payload parsing const { data, inferredType } = res; switch (data.type) { case "book.created": console.log("New Book Created:", data.title); break; case "book.deleted": console.log("Book Deleted:", data.title); break; default: console.warn(`Unhandled event type: ${inferredType}`); } } ```
```typescript filename="fern-example/webhook.ts" // techbooks-fern SDK created by Fern import FernClient from 'fern-sdk'; const client = new FernClient(); async function handleWebhook(req) { try { const payload = client.webhooks.constructEvent({ body: req.body, signature: req.headers['x-imdb-signature'], secret: process.env.WEBHOOK_SECRET }); if (payload.type === "book.created") { console.log("New Book Created:", payload.title); } else if (payload.type === "book.deleted") { console.log("Book Deleted:", payload.title); } else { console.warn(`Unhandled event type: ${payload.type}`); } } catch (error) { console.error("Webhook validation failed:", error); throw new Error("Invalid webhook signature"); } } ```
You can read more about how Speakeasy handles webhooks in our [webhooks release post](/post/release-webhooks-support). ### OAuth client credentials handling OAuth 2.0 client handling is only available on Fern's paid plans. Speakeasy supports OAuth 2.0 client credentials on all plans. Both Speakeasy and Fern generate SDKs that handle OAuth 2.0 with client credentials, offering similar functionality in managing the token lifecycle and authentication processes. Our bookstore API requires an OAuth 2.0 token with client credentials to access the API. Let's see how the SDKs handle this. Consider the following OAuth 2.0 configuration from the OpenAPI document: ```yaml components: securitySchemes: OAuth2: type: oauth2 flows: clientCredentials: tokenUrl: https://api.bookstore.com/oauth/token scopes: write: Grants write access read: Grants read access ``` Let's look at how you can use the SDKs to create a new book in the bookstore API and how to handle OAuth 2.0 authentication.
```typescript filename="example/techbooks-speakeasy.ts" import { TechBooks } from "techbooks-speakeasy"; const bookStore = new TechBooks({ security: { // OAuth 2.0 client credentials clientID: "", clientSecret: "", }, }); async function run() { // The SDK handles the token lifecycle, retries, and error handling for you await bookStore.books.addBook({ // Book object }); } run(); ```
```typescript filename="example/techbooks-fern.ts" import FernClient from 'fern-sdk'; const client = new FernClient({ clientId: '', clientSecret: '', }); async function run() { await client.books.addBook({ // Book object }); } run(); ```
Both Speakeasy and Fern SDKs handle OAuth 2.0 automatically – you provide credentials when creating the client, and the SDK manages token lifecycle, refreshes, and errors for you without additional manual handling. ## Fern-generated SDK case study: Cohere TypeScript Let's take a closer look at a real-world SDK generated by Fern for a more complete view of Fern's SDK generation. We inspected the [Cohere TypeScript SDK](https://github.com/cohere-ai/cohere-typescript) and here's what we found. ### SDK structure The Cohere SDK's repository is deeply nested, reminiscent of older Java codebases. This may reflect the generator's codebase, or it may be due to the generator's templates being designed by developers who aren't TypeScript specialists. There is a separation between core SDK code and API-specific code such as models and request methods, but internal SDK tools that hide behind layers of abstraction are not marked clearly as internal. This can lead to breaking changes in users' applications in the future. Speakeasy addresses these problems by clearly separating core internal code into separate files, or marking individual code blocks as clearly as possible for internal use. Repository structure and comments follow the best practices for each SDK's target platform, as designed by specialists in each platform. ### Data validation libraries Both Speakeasy and Fern generate SDKs that feature runtime data validation. We've observed that Speakeasy uses Zod, a popular and thoroughly tested data validation and schema declaration library. The Cohere TypeScript SDK, on the other hand, uses a custom Zod-like type-checking library, which ships as part of the SDK. Using a hand-rolled type library is a questionable practice for various reasons. Firstly, it ships type inference code as part of the SDK, which adds significant complexity. Here's an example of [date type inference](https://github.com/cohere-ai/cohere-typescript/blob/30ac11173e374e66310184834831cc5fca2256fc/src/core/schemas/builders/date/date.ts#L6-L8) using complex regular expression copied from Stack Overflow. ```typescript filename="src/core/schemas/builders/date/date.ts" import { BaseSchema, Schema, SchemaType } from "../../Schema"; import { getErrorMessageForIncorrectType } from "../../utils/getErrorMessageForIncorrectType"; import { maybeSkipValidation } from "../../utils/maybeSkipValidation"; import { getSchemaUtils } from "../schema-utils"; // https://stackoverflow.com/questions/12756159/regex-and-iso8601-formatted-datetime const ISO_8601_REGEX = /^([+-]?\d{4}(?!\d{2}\b))((-?)((0[1-9]|1[0-2])(\3([12]\d|0[1-9]|3[01]))?|W([0-4]\d|5[0-2])(-?[1-7])?|(00[1-9]|0[1-9]\d|[12]\d{2}|3([0-5]\d|6[1-6])))([T\s]((([01]\d|2[0-3])((:?)[0-5]\d)?|24:?00)([.,]\d+(?!:))?)?(\17[0-5]\d([.,]\d+)?)?([zZ]|([+-])([01]\d|2[0-3]):?([0-5]\d)?)?)?)?$/; ``` While Zod uses a similar regex-based approach to dates under the hood, we know that Zod's types and methods are widely used, tested by thousands of brilliant teams each day, and are supported by stellar [documentation](https://zod.dev/?id=dates). Furthermore, using Zod in SDKs created by Speakeasy allows users to include Zod as an external library when bundling their applications. Speakeasy bundles Zod as a regular dependency to prevent version conflicts and ensure consistent behavior. A hand-rolled type library will almost certainly lead to safety issues that are challenging to debug and impossible to find answers for from other developers, as there is no community support. ### Documentation Apart from a short README, the Cohere TypeScript SDK does not include any documentation. This is in stark contrast to SDKs created by Speakeasy, which contain copy-paste usage examples for all methods and documentation for each model. Speakeasy SDKs are also supported by Zod's detailed and clear documentation regarding types and validation. ### Readability SDK method bodies in the Cohere SDK are extremely long, unclear, and contain repeated verbose response-matching code. As a result, methods are difficult to read and understand at a glance. Response matching in SDK methods involves long switch statements that are repeated in each method. The snippet below from the Cohere SDK is repeated multiple times. ```typescript filename="src/Client.ts" // ... if (_response.error.reason === "status-code") { switch (_response.error.statusCode) { case 400: throw new Cohere.BadRequestError(_response.error.body); case 401: throw new Cohere.UnauthorizedError(_response.error.body); case 403: throw new Cohere.ForbiddenError(_response.error.body); case 404: throw new Cohere.NotFoundError(_response.error.body); case 422: throw new Cohere.UnprocessableEntityError( await serializers.UnprocessableEntityErrorBody.parseOrThrow( _response.error.body, { unrecognizedObjectKeys: "passthrough", allowUnrecognizedUnionMembers: true, allowUnrecognizedEnumValues: true, skipValidation: true, breadcrumbsPrefix: ["response"], }, ), ); case 429: throw new Cohere.TooManyRequestsError( await serializers.TooManyRequestsErrorBody.parseOrThrow( _response.error.body, { unrecognizedObjectKeys: "passthrough", allowUnrecognizedUnionMembers: true, allowUnrecognizedEnumValues: true, skipValidation: true, breadcrumbsPrefix: ["response"], }, ), ); case 499: throw new Cohere.ClientClosedRequestError( await serializers.ClientClosedRequestErrorBody.parseOrThrow( _response.error.body, { unrecognizedObjectKeys: "passthrough", allowUnrecognizedUnionMembers: true, allowUnrecognizedEnumValues: true, skipValidation: true, breadcrumbsPrefix: ["response"], }, ), ); case 500: throw new Cohere.InternalServerError(_response.error.body); case 501: throw new Cohere.NotImplementedError( await serializers.NotImplementedErrorBody.parseOrThrow( _response.error.body, { unrecognizedObjectKeys: "passthrough", allowUnrecognizedUnionMembers: true, allowUnrecognizedEnumValues: true, skipValidation: true, breadcrumbsPrefix: ["response"], }, ), ); case 503: throw new Cohere.ServiceUnavailableError(_response.error.body); case 504: throw new Cohere.GatewayTimeoutError( await serializers.GatewayTimeoutErrorBody.parseOrThrow( _response.error.body, { unrecognizedObjectKeys: "passthrough", allowUnrecognizedUnionMembers: true, allowUnrecognizedEnumValues: true, skipValidation: true, breadcrumbsPrefix: ["response"], }, ), ); default: throw new errors.CohereError({ statusCode: _response.error.statusCode, body: _response.error.body, }); } } switch (_response.error.reason) { case "non-json": throw new errors.CohereError({ statusCode: _response.error.statusCode, body: _response.error.rawBody, }); case "timeout": throw new errors.CohereTimeoutError(); case "unknown": throw new errors.CohereError({ message: _response.error.errorMessage, }); } // ... ``` By contrast, Speakeasy creates SDKs with improved readability by breaking SDK functionality into smaller, more focused methods, without hiding important steps behind multiple layers of abstraction. ### Open enums Both Speakeasy and Fern generate SDKs that allow users to pass unknown values in fields that are defined as enums if the SDK is configured to do so. This is useful to keep legacy SDKs working when an API changes. However, where Speakeasy SDKs clearly mark unknown enum values by wrapping them in an `Unrecognized` type, SDKs generated by Fern use a type assertion. By not marking unrecognized enum values as such, Fern undermines the type safety TypeScript users rely on. Consider the following OpenAPI component: ```yaml filename="openapi.yaml" components: schemas: BackgroundColor: type: string x-speakeasy-unknown-values: allow enum: - red - green - blue ``` Based on this definition, Speakeasy will allow users to set the value of the `BackgroundColor` string to `yellow`, but will mark it as unrecognized. Here's an example of what this looks like in TypeScript: ```typescript filename="speakeasy/BackgroundColor.ts" type BackgroundColor = "red" | "green" | "blue" | Unrecognized; ``` In the Cohere SDK generated by Fern, we found this enum: ```typescript filename="src/serialization/resources/finetuning/resources/finetuning/types/Status.ts" /** * This file was autogenerated by Fern from our API Definition. */ import * as Cohere from "../../../../../../api/index"; import * as core from "../../../../../../core"; import * as serializers from "../../../../../index"; export const Status: core.serialization.Schema< serializers.finetuning.Status.Raw, Cohere.finetuning.Status > = core.serialization.enum_([ "STATUS_UNSPECIFIED", "STATUS_FINETUNING", "STATUS_DEPLOYING_API", "STATUS_READY", "STATUS_FAILED", "STATUS_DELETED", "STATUS_TEMPORARILY_OFFLINE", "STATUS_PAUSED", "STATUS_QUEUED", ]); export declare namespace Status { type Raw = | "STATUS_UNSPECIFIED" | "STATUS_FINETUNING" | "STATUS_DEPLOYING_API" | "STATUS_READY" | "STATUS_FAILED" | "STATUS_DELETED" | "STATUS_TEMPORARILY_OFFLINE" | "STATUS_PAUSED" | "STATUS_QUEUED"; } ``` When we looked at the definition of `core.serialization.enum_`, we found that any string value can be passed as a status, and would be represented as type `Status`. ## SDK and bundle size Both Speakeasy and Fern SDKs include runtime data validation, which can increase the bundle size. However, Speakeasy SDKs are designed to be tree-shakable, so you can remove any unused code from the SDK before bundling it. Speakeasy also exposes a standalone function for each API call, which allows you to import only the functions you need, further reducing the bundle size. ### Creating bundles Let's compare the bundle sizes of the SDKs generated by Speakeasy and Fern. Start by adding a `speakeasy.ts` file that imports the Speakeasy SDK: ```typescript filename="speakeasy.ts" import { SDKCore } from "./bar-sdk-speakeasy/src/core"; import { drinksListDrinks } from "./bar-sdk-speakeasy/src/funcs/drinksListDrinks"; // Use `SDKCore` for best tree-shaking performance. // You can create one instance of it to use across an application. const sdk = new SDKCore(); async function run() { const res = await drinksListDrinks(sdk, {}); if (!res.ok) { throw res.error; } const { value: result } = res; // Handle the result console.log(result); } run(); ``` Next, add a `fern.ts` file that imports the Fern SDK: ```typescript filename="fern.ts" import { NdimaresApiClient } from "./generated/typescript"; // Create an instance of the Fern SDK client const client = new NdimaresApiClient({ apiKey: "YOUR_API_KEY", }); async function run() { try { // Use the drinks.listDrinks() method to get the list of drinks const result = await client.drinks.listDrinks(); // Handle the result console.log(result); } catch (error) { console.error("An error occurred:", error); } } run(); ``` We'll use esbuild to bundle the SDKs. First, install esbuild: ```bash npm install esbuild ``` Next, add a `build.js` script that uses esbuild to bundle the SDKs: ```typescript filename="build.js" import * as fs from "fs"; import * as esbuild from "esbuild"; const speakeasyBuild = await esbuild.build({ entryPoints: ["speakeasy.ts"], outfile: "dist/speakeasy.js", bundle: true, minify: true, treeShaking: true, metafile: true, target: "node18", platform: "node", }); fs.writeFileSync( "dist/speakeasy.json", JSON.stringify(speakeasyBuild.metafile, null, 2), ); const fernBuild = await esbuild.build({ entryPoints: ["fern.ts"], outfile: "dist/fern.js", bundle: true, minify: true, treeShaking: true, metafile: true, target: "node18", platform: "node", }); fs.writeFileSync("dist/fern.json", JSON.stringify(fernBuild.metafile, null, 2)); ``` Run the `build.js` script: ```bash node build.js ``` This generates two bundles, `dist/speakeasy.js` and `dist/fern.js`, along with their respective metafiles. ### Bundle size comparison Now that we have two bundles, let's compare their sizes. First, let's look at the size of the `dist/speakeasy.js` bundle: ```bash du -sh dist/speakeasy.js # Output # 76K dist/speakeasy.js ``` Next, let's look at the size of the `dist/fern.js` bundle: ```bash du -sh dist/fern.js # Output # 232K dist/fern.js ``` The SDK generated by Fern is significantly larger than that built with the SDK generated by Speakeasy. We can use the metafiles generated by esbuild to analyze the bundle sizes in more detail. ### Analyzing bundle sizes The metafiles generated by esbuild contain detailed information about which source files contribute to each bundle's size, presented as a tree structure. We used esbuild's online [bundle visualizer](https://esbuild.github.io/analyze/) to analyze the bundle sizes. Here's a summary of the bundle sizes: The `dist/speakeasy.js` bundle's largest contributor, at 72.3%, is the Zod library used for runtime data validation. The Zod library's tree-shaking capabilities are a work in progress, and future versions of SDKs are expected to have smaller bundle sizes. ![Speakeasy bundle size](/assets/blog/speakeasy-vs-fern/speakeasy-bundle-size.png) The `dist/fern.js` bundle includes bundled versions of `node-fetch`, polyfills, and other dependencies, which contribute to the larger bundle size. Fern's SDKs also include custom serialization code and a validation library, which can increase the bundle size. ![Fern bundle size](/assets/blog/speakeasy-vs-fern/fern-bundle-size.png) ### Bundling for the browser Speakeasy SDKs are designed to work in a range of environments, including the browser. To bundle an SDK for the browser, you can use a tool like esbuild or webpack. Here's an example of how to bundle the Speakeasy SDK for the browser using esbuild: ```bash npx esbuild src/speakeasy.ts --bundle --minify --target=es2020 --platform=browser --outfile=dist/speakeasy-web.js ``` Doing the same for the Fern SDK generates an error, as the SDK is not designed to work in the browser out of the box. ## Summary Speakeasy's additional language support and SDK documentation make it a better choice than Fern for most users. If you are interested in seeing how Speakeasy stacks up against other SDK generation tools, check out our [post](https://www.speakeasy.com/post/compare-speakeasy-open-source). # speakeasy-vs-liblab Source: https://speakeasy.com/blog/speakeasy-vs-liblab import { Table } from "@/mdx/components"; import { FileTree } from "nextra/components"; This analysis compares Speakeasy and liblab in terms of SDK generation, OpenAPI integration, language support, features, platform support, enterprise support, and pricing. We'll also provide a technical walkthrough of generating SDKs using both services and compare the generated TypeScript SDKs. ## In short: How do Speakeasy and liblab differ? 1. **OpenAPI integration:** Speakeasy is [built for OpenAPI](/docs/openapi/openapi-support), supporting advanced features of OpenAPI 3.0 and 3.1, while testing and implementing upcoming OpenAPI features like OpenAPI [Overlays](/openapi/overlays) and [Workflows](/openapi/arazzo). liblab uses OpenAPI as a starting point, but has not yet implemented advanced OpenAPI features like Overlays or Workflows, instead depending on its own configuration system. This can lead to a divergence between the OpenAPI document and the generated SDK, and may require additional configuration after spec changes. 2. **Velocity and language support**: liblab was founded in January 2022 and gradually expanded its language support and features to support seven languages. In comparison, Speakeasy was founded in May 2022, found market traction in early 2023, and released support for ten languages within 12 months. Speakeasy meets the diverse needs of users, while supporting their existing stacks. 3. **SDK generator maturity:** Speakeasy creates SDKs that are [idiomatic to each target language](/docs/sdk-design/intro), type safe during development and production, human-readable, and fault-tolerant. Our comparison found some room for improvement in liblab's type safety, fault tolerance, and SDK project structure. Both products are under active development, and improvement should be expected. ## Comparing Speakeasy and liblab We'll start by comparing the two services in terms of their SDK generation targets, features, platform support, enterprise support, and pricing. ### SDK generation targets As your user base grows, the diversity of their technical requirements will expand beyond the proficiencies of your team. At Speakeasy, we understand the importance of enabling users to onboard with our clients without making major changes to their tech stacks. Our solution is to offer support for a wide range of SDK generation targets. The table below highlights the current SDK language support offered by Speakeasy and liblab as of June 2024. Please note that these lists are subject to change, so always refer to the official documentation for the most up-to-date information.
Everyone has that one odd language that is critical to their business. In our first year, we've made a dent, but we've got further to go. See a language that you require that we don't support? [Join our Slack community](https://go.speakeasy.com/slack) to let us know. ### SDK features This table shows the current feature support for Speakeasy and liblab as of June 2024. Refer to the official documentation for the most recent updates.
Speakeasy creates SDKs that handle advanced authentication. For example, Speakeasy can generate SDKs that handle OAuth 2.0 with client credentials - handling the token lifecycle, retries, and error handling for you. liblab leaves some of these features to be implemented by the user. For example, liblab's configuration allows for global retries on all operations, but only recently released support for custom retries requiring custom implementation per SDK. liblab also lacks support for pagination, server-sent events, and streaming uploads. ### Platform features Speakeasy is designed to be used locally, with a dependency-free CLI that allows for local experimentation and iteration. This makes it easier to test and iterate on your SDKs and allows for custom CI/CD workflows. For local use, liblab provides an NPM package with a dependency tree of 749 modules. Installing and updating the liblab CLI is slower than Speakeasy's single binary, the CLI is less feature-rich, and it depends on Node.js and NPM.
### Enterprise support Both Speakeasy and liblab offer support for Enterprise customers. This includes features like concierge onboarding, private Slack channels, and enterprise SLAs.
### Pricing Speakeasy offers a free tier, with paid plans starting at $600 per month. liblab's free tier is limited to open-source projects, with paid plans starting at $300 per month. Both services offer custom enterprise plans.
## TypeScript SDK comparison Now that we have two TypeScript SDKs generated from a single OpenAPI document, let's see how they differ. ### SDK structure overview Before we dive into the detail, let's get an overall view of the default project structure for each SDK. Speakeasy automatically generates detailed documentation with examples for each operation and component, while liblab generates an `examples` folder with a single example. liblab generates a `test` folder, while Speakeasy does not. We'll take a closer look at this shortly. In the comparison below, comparing the folder structure might seem superficial at first, but keep in mind that SDK users get the same kind of high-level glance as their first impression of your SDK. Some of this may be a matter of opinion, but at Speakeasy we aim to generate SDKs that are as organized as SDKs coded by hand.
#### Speakeasy SDK structure Speakeasy generates separate folders for models and operations, both in the documentation and in the source folder. This indicates a clear separation of concerns. We also see separate files for each component and operation, indicating modularity and separation of concerns.
#### liblab SDK structure liblab generates an SDK that at a glance looks less organized, considering the greater number of configuration files at the root of the project, the lack of a `docs` folder, and the way the `src` folder is structured by OpenAPI tags instead of models and operations.
### SDK code comparison With the bird's-eye view out of the way, let's take a closer look at the code. #### Runtime type checking Speakeasy creates SDKs that are type safe from development to production. As our CEO recently wrote, [Type Safe is better than Type Faith](/post/type-safe-vs-type-faith). The SDK created by Speakeasy uses [Zod](https://zod.dev/) to validate data at runtime. Data sent to the server and data received from the server are validated against Zod definitions in the client. This provides safer runtime code execution and helps developers who use your SDK to provide early feedback about data entered by their end users. Furthermore, trusting data validation on the client side allows developers more confidence to build [optimistic UIs](https://medium.com/distant-horizons/using-optimistic-ui-to-delight-your-users-ac819a81d59a) that update as soon as an end user enters data, greatly improving end users' perception of your API's speed. Let's see how Speakeasy's runtime type checking works in an example. Consider the following `Book` component from our OpenAPI document: ```yaml mark=18:21 !from ./openapi.yaml.txt 379:406 ``` The highlighted `price` field above has type `integer`. ```typescript filename="speakeasy-example.ts" mark=17 // techbooks-speakeasy SDK created by Speakeasy import { TechBooks } from "techbooks-speakeasy"; const bookStore = new TechBooks({ apiKey: "123", }); async function run() { await bookStore.books.addBook({ author: { name: "Robert C. Martin", photo: "https://example.com/photos/robert.jpg", biography: 'Robert Cecil Martin, colloquially known as "Uncle Bob", is an American software engineer...', }, category: "Programming", description: "A Handbook of Agile Software Craftsmanship", price: 29.99, title: "Clean Code", }); } run(); ``` The `price` field in the `Book` object in our test code is set to `29.99`, which is a floating-point number. This will cause a validation error before the data is sent to the server, as the `price` field is expected to be an integer. [Handling Zod validation errors](https://zod.dev/?id=error-handling) is straightforward, and allows developers to provide meaningful feedback to their end users early in the process. The same book object in code using the SDK generated by liblab will only be validated on the server. This means that the error will only be caught from the client's perspective _after_ the data is sent to the server, and the server responds with an error message. If the server is not set up to validate the `price` field, the error will _not be caught at all_, leading to unexpected behavior in your developer-users' applications. As a result, developers using the SDK generated by liblab may need to write additional client-side validation code to catch these errors before they are sent to the server. #### Components compared Speakeasy creates SDKs that contain rich type information for each component in your OpenAPI document. This includes clear definitions of enums, discriminated unions, and other complex types, augmented with runtime validation using Zod. Let's compare the `Order` component from our OpenAPI document in the SDKs generated by Speakeasy and liblab. Here's the `Order` component as generated by liblab: ```typescript filename="liblab/services/orders/models/Order.ts" // This file was generated by liblab | https://liblab.com/ import { FantasyBook } from "../../common/FantasyBook"; import { ProgrammingBook } from "../../common/ProgrammingBook"; import { SciFiBook } from "../../common/SciFiBook"; import { User } from "./User"; type Status = "pending" | "shipped" | "delivered"; export interface Order { id: number; date: string; status: Status; user: User; products: (FantasyBook | ProgrammingBook | SciFiBook)[]; } ``` Note how the `products` field is defined as an array of `FantasyBook`, `ProgrammingBook`, or `SciFiBook`. This is a union type, but the SDK generated by liblab does not provide any runtime validation for this field, nor does it give any indication that this is a discriminated union. Contrast this with the `Order` component as created by Speakeasy, which exports a useful `Products` type that is a discriminated union of `FantasyBook`, `ProgrammingBook`, and `SciFiBook`, along with a `Status` enum for use elsewhere in your code. Verbose runtime validation is marked as `@internal` in the generated code, clearly indicating that it is not intended for direct use by developers, but rather for internal use by the SDK. Here's the `Order` component created by Speakeasy: ```typescript filename="speakeasy/models/components/order.ts" /* * Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT. */ import * as z from "zod"; import { FantasyBook, FantasyBook$ } from "./fantasybook"; import { ProgrammingBook, ProgrammingBook$ } from "./programmingbook"; import { SciFiBook, SciFiBook$ } from "./scifibook"; import { User, User$ } from "./user"; export enum Status { Pending = "pending", Shipped = "shipped", Delivered = "delivered", } export type Products = | (ProgrammingBook & { category: "Programming" }) | (FantasyBook & { category: "Fantasy" }) | (SciFiBook & { category: "Sci-fi" }); export type Order = { id: number; date: Date; status: Status; user: User; products: Array< | (ProgrammingBook & { category: "Programming" }) | (FantasyBook & { category: "Fantasy" }) | (SciFiBook & { category: "Sci-fi" }) >; }; /** @internal */ export namespace Status$ { export const inboundSchema = z.nativeEnum(Status); export const outboundSchema = inboundSchema; } /** @internal */ export namespace Products$ { export const inboundSchema: z.ZodType = z.union([ ProgrammingBook$.inboundSchema.and( z .object({ category: z.literal("Programming") }) .transform((v) => ({ category: v.category })), ), FantasyBook$.inboundSchema.and( z .object({ category: z.literal("Fantasy") }) .transform((v) => ({ category: v.category })), ), SciFiBook$.inboundSchema.and( z .object({ category: z.literal("Sci-fi") }) .transform((v) => ({ category: v.category })), ), ]); export type Outbound = | (ProgrammingBook$.Outbound & { category: "Programming" }) | (FantasyBook$.Outbound & { category: "Fantasy" }) | (SciFiBook$.Outbound & { category: "Sci-fi" }); export const outboundSchema: z.ZodType = z.union([ ProgrammingBook$.outboundSchema.and( z .object({ category: z.literal("Programming") }) .transform((v) => ({ category: v.category })), ), FantasyBook$.outboundSchema.and( z .object({ category: z.literal("Fantasy") }) .transform((v) => ({ category: v.category })), ), SciFiBook$.outboundSchema.and( z .object({ category: z.literal("Sci-fi") }) .transform((v) => ({ category: v.category })), ), ]); } /** @internal */ export namespace Order$ { export const inboundSchema: z.ZodType = z.object({ id: z.number().int(), date: z .string() .datetime({ offset: true }) .transform((v) => new Date(v)), status: Status$.inboundSchema, user: User$.inboundSchema, products: z.array( z.union([ ProgrammingBook$.inboundSchema.and( z .object({ category: z.literal("Programming") }) .transform((v) => ({ category: v.category })), ), FantasyBook$.inboundSchema.and( z .object({ category: z.literal("Fantasy") }) .transform((v) => ({ category: v.category })), ), SciFiBook$.inboundSchema.and( z .object({ category: z.literal("Sci-fi") }) .transform((v) => ({ category: v.category })), ), ]), ), }); export type Outbound = { id: number; date: string; status: string; user: User$.Outbound; products: Array< | (ProgrammingBook$.Outbound & { category: "Programming" }) | (FantasyBook$.Outbound & { category: "Fantasy" }) | (SciFiBook$.Outbound & { category: "Sci-fi" }) >; }; export const outboundSchema: z.ZodType = z.object({ id: z.number().int(), date: z.date().transform((v) => v.toISOString()), status: Status$.outboundSchema, user: User$.outboundSchema, products: z.array( z.union([ ProgrammingBook$.outboundSchema.and( z .object({ category: z.literal("Programming") }) .transform((v) => ({ category: v.category })), ), FantasyBook$.outboundSchema.and( z .object({ category: z.literal("Fantasy") }) .transform((v) => ({ category: v.category })), ), SciFiBook$.outboundSchema.and( z .object({ category: z.literal("Sci-fi") }) .transform((v) => ({ category: v.category })), ), ]), ), }); } ``` #### OAuth client credentials handling Only Speakeasy's SDKs handle OAuth 2.0 with client credentials, including managing the token lifecycle, retries, and error handling without any additional code. Our bookstore API requires an OAuth 2.0 token with client credentials to access the API. Let's see how the SDKs handle this. Consider the following OAuth 2.0 configuration from our OpenAPI document: ```yaml filename="openapi.yaml" !from ./openapi.yaml.txt 633:639 ``` Speakeasy's generated SDK takes a `clientID` and `clientSecret` when instantiating the SDK. The SDK also includes `ClientCredentialsHook` class that implements `BeforeRequestHook` to check whether the token is expired and refresh it if necessary. The hook also checks whether the client has the necessary scopes to access the endpoint, and handles authentication errors. ```typescript filename="speakeasy-example.ts" // techbooks-speakeasy SDK created by Speakeasy import { TechBooks } from "techbooks-speakeasy"; const bookStore = new TechBooks({ security: { // OAuth 2.0 client credentials clientID: "", clientSecret: "", }, }); async function run() { // The SDK handles the token lifecycle, retries, and error handling for you await bookStore.books.addBook({ // Book object }); } run(); ``` The SDK generated by liblab does not support OAuth 2.0 client credentials at all. #### Server-sent events (SSE) and streaming responses Our bookstore API includes an operation that streams orders to the client using Server-Sent Events (SSE). ```yaml mark=15 paths: /orderstream: get: summary: Get a stream of orders operationId: getOrderStream description: Returns a stream of orders tags: - Orders security: - apiKey: [] responses: "200": description: A stream of orders content: text/event-stream: schema: $ref: "#/components/schemas/OrderStreamMessage" ``` Let's see how the SDKs handle this. [Speakeasy generates types and methods for handling SSE](/docs/customize-sdks/streaming) without any customization. Here's an example of how to use the SDK to listen for new orders: ```typescript filename="speakeasy-example.ts" import { TechBooks } from "techbooks-speakeasy"; const bookStore = new TechBooks({ apiKey: "KEY123", }); async function run() { const result = await bookStore.orders.getOrderStream(); if (result.orderStreamMessage == null) { throw new Error("Failed to create stream: received null value"); } const stream = result.orderStreamMessage.stream; if (!stream || typeof stream.getReader !== "function") { throw new Error("Invalid stream: expected a ReadableStream"); } const reader = stream.getReader(); try { while (true) { const { done, value } = await reader.read(); if (done) break; console.log(new TextDecoder().decode(value)); } } catch (error) { console.error("Error reading stream", error); } finally { reader.releaseLock(); } } run(); ``` (The example above does not run against a local Prism server, but you can test it against [Stoplight's hosted Prism](https://stoplight.io/) server.) liblab does not generate SSE handling code. Developers using the SDK generated by liblab will need to write additional code to handle SSE. #### Streaming uploads Speakeasy supports streaming uploads without any custom configuration. OpenAPI operations with `multipart/form-data` content types are automatically handled as streaming uploads. The following example illustrates how to use an SDK created by Speakeasy to upload a large file: ```typescript filename="speakeasy-example.ts" import { openAsBlob } from "node:fs"; import { SDK } from "@speakeasy/super-sdk"; async function run() { const sdk = new SDK(); const fileHandle = await openAsBlob("./src/sample.txt"); const result = await sdk.upload({ file: fileHandle }); console.log(result); } run(); ``` #### React Hooks React Hooks simplify state and data management in React apps, enabling developers to consume APIs more efficiently. liblab does not support React Hooks natively. Developers must manually integrate SDK methods into state management tools like React Context, Redux, or TanStack Query. Speakeasy generates built-in React Hooks using [TanStack Query](https://tanstack.com/query/latest). These hooks provide features like intelligent caching, type safety, pagination, and seamless integration with modern React patterns such as SSR and Suspense. Here's an example: ```typescript filename="example/loadPosts.tsx" import { useQuery } from "@tanstack/react-query"; function Posts() { const { data, status, error } = useQuery([ "posts" // Cache key for the query ], async () => { const response = await fetch("https://jsonplaceholder.typicode.com/posts"); return response.json(); }); if (status === "loading") return

Loading posts...

; if (status === "error") return

Error: {error?.message}

; return (
    {data.map((post) => (
  • {post.title}
  • ))}
); } ``` For example, in this basic implementation, the `useQuery` hook fetches data from an API endpoint. The cache key ensures unique identification of the query. The `status` variable provides the current state of the query: `loading`, `error`, or `success`. Depending on the query status, the component renders `loading`, `error`, or the fetched data as a list. #### Auto-pagination Speakeasy's React Hooks also enable auto-pagination, which automatically fetches more data when the user scrolls to the bottom of the page. This feature is useful for infinite scrolling in social media feeds or search results. ```typescript filename="example/PostsView.tsx" import { useInView } from "react-intersection-observer"; import { useActorAuthorFeedInfinite } from "@speakeasy-api/bluesky/react-query/actorAuthorFeed.js"; export function PostsView(props: { did: string }) { const { data, fetchNextPage, hasNextPage } = useActorAuthorFeedInfinite({ actor: props.did, }); const { ref } = useInView({ rootMargin: "50px", onChange(inView) { if (inView) { fetchNextPage(); } }, }); return (
    {data?.pages.flatMap((page) => { return page.result.feed.map((entry) => (
  • )); })}
{hasNextPage ?
: null}
); ``` liblab also supports pagination, however this requires additional configuration using the `liblab.config.json` file. For an in-depth look at how Speakeasy generates React Hooks, see our [official release article](https://www.speakeasy.com/post/release-react-hooks). ### OpenAPI extensions and Overlays vs liblab config Speakeasy embraces OpenAPI as the source of truth for generating SDKs. This means that Speakeasy does not require any additional configuration files to generate SDKs, apart from minimal configuration in the `gen.yaml` file. Any configuration related to individual operations or components is done in the OpenAPI document itself, using OpenAPI extensions. Speakeasy provides a [list of supported OpenAPI extensions](/docs/customize-sdks) in its documentation. If editing your OpenAPI document is not an option, Speakeasy also supports the [OpenAPI Overlays](/docs/prep-openapi/overlays/create-overlays) specification, which allows you to add or override parts of an OpenAPI document without modifying the original document. This step can form part of your CI/CD pipeline, ensuring that your SDKs are always up-to-date with your API, even if your OpenAPI document is generated from code. Speakeasy's CLI can also generate OpenAPI Overlays for you, based on the differences between two OpenAPI documents. Instead of using OpenAPI extensions, liblab uses a [configuration file](https://developers.liblab.com/cli/config-file-overview/) to customize SDKs. This configuration overrides many of the aspects Speakeasy allows you to configure in the OpenAPI document itself, or using overlays. ### Linting and change detection Speakeasy's CLI includes a detailed and accurate linter that checks your OpenAPI document and provides feedback. This is especially useful during development, but can also catch errors in your CI/CD pipeline. Speakeasy also keeps track of changes in your OpenAPI document, and versions the SDKs it creates based on those changes. ### Building for the browser Both Speakeasy and liblab generate SDKs that can be used in the browser. To use the SDKs in the browser, you need to bundle your application using a tool like webpack, Rollup, or esbuild. Speakeasy creates SDKs that are tree-shakeable, meaning that you can include only the parts of the SDK that you need in your application. This can help reduce the size of your application bundle. Because Speakeasy SDKs include runtime type checking, the Zod library is included in the bundle. However, if you use Zod in other parts of your application, you can share the Zod instance between the SDK and your application to reduce the bundle size. Here's an example of how to bundle an application that uses the Speakeasy SDK for the browser without Zod using esbuild: ```bash filename="Terminal" mark=7 npx esbuild src/speakeasy-app.ts \ --bundle \ --minify \ --target=es2020 \ --platform=browser \ --outfile=dist/speakeasy-app.js \ --external:zod ``` ## Speakeasy compared to open-source generators If you are interested in seeing how Speakeasy stacks up against other SDK generation tools, check out our [post](/post/compare-speakeasy-open-source). # speakeasy-vs-openapi-generator Source: https://speakeasy.com/blog/speakeasy-vs-openapi-generator import { CardGrid } from "@/components/card-grid"; import { openApiGeneratorComparisonData } from "@/lib/data/blog/openapi-generator-comparison"; import { Callout, Table } from "@/mdx/components"; ## OpenAPI Generation Overview The [OpenAPI Generator project](https://openapi-generator.tech/) is a popular open-source tool for generating SDKs from OpenAPI/Swagger specifications. It has been an indispensable tool for the increasing number of companies which provide SDKs to their users. But, as any past user will know, there is a gap between what it is possible to achieve with off-the-shelf OSS and what's required for supporting enterprise APIs. Simply put, when users are paying for your API, they expect (and deserve) a high quality SDK: type-safety, authentication, pagination, retries and more. That is why **Speakeasy** was created. The platform has been designed to generate a best in class DevEx that can support enterprise APIs of any size & complexity. In this article, we'll dive into the business and high level technical differences between using Speakeasy to generate SDKs and using the OpenAPI Generators. If you want a granular breakdown for a specific language, check out our comparison guides. ## The cost of using `openapi-generator` {/* “OpenAPI gen doesn't work very well — the code often doesn't look and feel right, and certain aspects of OpenAPI just aren't handled well. We would probably still have needed to spend a ton of engineer time working around its limitations and quirks, and we decided that the headache wasn't worth it.” */} Open-source software is free, but that doesn't mean it doesn't have a cost. The open-source projects have a number of issues that make it difficult to use in an enterprise: - **Spotty API feature coverage**: There are holes in the OpenAPI generators that make it difficult to support moderately complex APIs. If your API uses OAuth, if it has union types, if it requires pagination, you'll find that the generators fall short. - **Non-idiomatic code**: The Open Source generators started off as a Java-based project. They are largely maintained by Java developers and consequently, many of the supported languages have a Java-like feel to them. This might not seem like a big deal, but it is massively off-putting to users if they are used to a language like Python or Typescript. - **Huge number of bugs**: There are 4.5K open issues as of January 2025 — and growing ~10% every year. The result of all these issues is that most enterprise users end up forking the repository and maintaining their own version of the Open Source generators with the fixes they need. ## Enterprise case study: using `openapi-generator` at Fastly Don't just take our (or even our customers') word for it, however. See what the API Architect of a publicly traded company [posted](https://dev.to/fastly/better-fastly-api-clients-with-openapi-generator-3lno) in Nov 2022 to mark his team's completion of a project to develop SDKs using the Open Source generators. The post is equal parts celebration of the accomplishment and postmortem on the pain suffered. Here are just a few key quotes from the post: {/* “To ensure we would be able to run the code-generation process within our Continuous Integration (CI) pipeline, we needed to use a Dockerised version of openapi-generator-cli (the coordination of which was a complex process).” */} {/* “Another challenge we encountered was the quality of the community-driven language templates. They worked but templates didn't appear to be well maintained over time, nor did every feature in our OpenAPI specification have a corresponding implementation in the language templates.” */} {/* “Language templates **didn't support the latest programming language version**, and the generated documentation wasn't as good as we needed it to be.” */} {/* “Our OpenAPI specification files would cause problems in the generator... Each of these issues would result in hard-to-debug compilation errors within the code-generation pipeline, and so we would have to figure out which of the three potential layers the issue was stemming from: 1. The Fastly OpenAPI specification documents. 2. The [openapi-generator mustache templates](https://github.com/OpenAPITools/openapi-generator/tree/master/modules/openapi-generator/src/main/resources). 3. The [openapi-generator language parsers](https://github.com/OpenAPITools/openapi-generator/tree/master/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages) (written in Java).” */} {/* “We had to iterate on both ends of the integration, a bit like trying to fit a square peg into a round hole, then gradually changing the shape of both the peg and the hole until they fit.” */} {/* “Auto-generating our API clients has been a long project... The code generation itself hasn't been easy: we've had to balance the complexity of coordinating changes across multiple repositories while handling errors over multiple layers, and producing API clients that are not only idiomatic to the language they're built in but also correct and accessible.” */} For an enterprise that wants to appeal to developers and showcase a compelling API experience, using Open Source successfully will require a significant investment in engineering resources, and significant time-to-market. Total cost of ownership for OSS tooling is likely to be 3+ FTEs at a minimum, depending on the API surface area and SDK languages required. This is an ongoing commitment. ## Generating handwritten SDKs with Speakeasy When we set out to build our SDK generators, we wanted to deliver something that was 10x better than the existing generators. Our goal was to generate handwritten SDKs. What constitutes a great SDK is somewhat subjective, but we set out some principles to guide development: - **Type safe** - The SDKs we generate are fully typed, so that developers can catch errors early and often. - **Human readable** - The SDKs we generate are easy for developers to read and debug. No convoluted abstractions. - **Minimal dependencies** - Our SDKs are meant to be powerful but lean. We start with native language libraries and layer on third-party libraries only when the customer benefits far outweigh the cost of the extra dependency. We avoid adding unnecessary dependencies. - **Batteries-included** - The SDKs we generate include everything from oauth, to pagination and retries. With every language we support, we're striving for an experience that's instantly familiar to developers. As an example, check out this code snippet from [Vercel's TypeScript SDK](https://github.com/vercel/sdk/tree/main), generated on the Speakeasy platform: ```typescript filename="buyDomain.ts" import { Vercel } from "@vercel/sdk"; const vercel = new Vercel({ bearerToken: "", }); async function run() { const result = await vercel.domains.buyDomain({ teamId: "team_1a2b3c4d5e6f7g8h9i0j1k2l", slug: "my-team-url-slug", requestBody: { name: "example.com", expectedPrice: 10, renew: true, country: "US", orgName: "Acme Inc.", firstName: "Jane", lastName: "Doe", address1: "340 S Lemon Ave Suite 4133", city: "San Francisco", state: "CA", postalCode: "91789", phone: "+1.4158551452", email: "jane.doe@someplace.com", }, }); // Handle the result console.log(result); } run(); ``` ## Generator Comparison Table
## Summary The OpenAPI Generators are great for the ecosystem, because they establish a baseline. But our commitment at Speakeasy is create a platform that is 10x better than using `openapi-generator`. We encourage you to try it out for yourself and see the difference. # speakeasy-vs-stainless Source: https://speakeasy.com/blog/speakeasy-vs-stainless import { Callout, Table } from "@/mdx/components"; import { FileTree } from "nextra/components"; This comparison of Speakeasy & Stainless reflects the current state of both platforms as of September 2025. Both companies continue to evolve rapidly, particularly around AI-agent integration and MCP capabilities. If you think we need to update this post, please let us know! In this post, we'll compare generated SDKs, as well as the underlying philosophies that guide the development of the two companies. And while we acknowledge that our views may be biased, we'll show our work along with our conclusions so that readers can decide for themselves. ## In short: How is Speakeasy different? ### OpenAPI-native vs OpenAPI-compatible Speakeasy is OpenAPI-native; Stainless is OpenAPI-compatible. Stainless is built on a custom DSL known as the [Stainless config](https://app.stainlessapi.com/docs/reference/config). This approach requires your team to manage an additional config file. Speakeasy has no intermediary DSL. Your OpenAPI spec is the only source of truth for SDK generation. Being OpenAPI-native is beneficial for integration into an existing stack. Regardless of the API proxies, server-side code, or specific web framework that you're using, you can plug in Speakeasy's tools. Stainless is doubling down on a vertically integrated approach by building a [backend TypeScript framework](https://github.com/stainless-api/stl-api) which will become the foundation for their solution. Their product will work best when you buy the entire stack, Speakeasy will shine regardless of the other tools in your existing stack. ### Type-safe vs type-faith There's [more on this topic](#runtime-type-checking) in the technical deep dive below. Speakeasy SDKs guarantee true end-to-end type safety with runtime validation using Zod schemas, meaning that types are generated to validate both request and response objects defined in your API. Stainless SDKs, on the other hand, are mainly type-hinted, not guarding the API from bad inputs. ### Velocity and maturity [Stainless was founded in 2021](https://www.sequoiacap.com/companies/stainless/) and has expanded its language support to seven languages. By comparison, Speakeasy launched in October 2022 and has released support for ten languages in less time. The Speakeasy platform is also broader, with support for additional generation features, like [webhooks](/post/release-webhooks-support), [React Hooks](/post/release-react-hooks), and [contract testing](/post/release-contract-testing), not supported by Stainless. Both companies are financially secure, having raised $25M+ in funding. ## Platform
## SDK generation
If there's a language you require that we don't support, [add it to our roadmap](https://roadmap.speakeasy.com/roadmap). ### SDK features Breadth matters, but so does the depth of support for each language. The table below shows the current feature support for Speakeasy and Stainless's SDK generation.
## Pricing In terms of pricing, both Speakeasy and Stainless offer free plans, as well as paid plans for startups and enterprises. The most significant pricing difference is in the enterprise plan. Existing customers indicate that Stainless's enterprise pricing is ~20% higher than Speakeasy's. Of course, this can vary, and we recommend reaching out to both companies for a quote.
## Speakeasy vs Stainless: TypeScript SDK comparison For this technical comparison, we'll examine the Cloudflare TypeScript SDK (generated by Stainless) and the Vercel SDK (generated by Speakeasy) to demonstrate real-world differences between these two approaches to SDK generation.
## SDK structure
### Vercel SDK (Speakeasy)
### Cloudflare SDK (Stainless)
The Vercel SDK (generated by Speakeasy) maintains a clear separation of concerns with dedicated folders for models, operations, hooks, and comprehensive documentation. Each component and operation has its own file, making the codebase easy to navigate and understand. The Cloudflare SDK (generated by Stainless) organizes code differently, with a flatter structure under `resources` and extensive runtime shims for cross-platform compatibility. While this approach supports multiple JavaScript runtimes, it results in more configuration files and less clear separation between different types of code. Structure affects developer experience. SDK users form their first impressions from the high-level organization, and a well-structured SDK suggests quality and maintainability to developers evaluating whether to adopt it. ## Dependencies and bundle size One key difference between Speakeasy and Stainless approaches is their dependency philosophy, which directly impacts bundle size and security surface area.
### Vercel SDK (Speakeasy) - Minimal dependencies ![Vercel SDK Dependencies](/assets/blog/speakeasy-vs-stainless/vercel-dependencies.png) The Vercel SDK has an extremely minimal dependency graph with only **Zod** as its single runtime dependency. This focused approach provides: - **Reduced attack surface**: Fewer dependencies mean fewer potential security vulnerabilities - **Predictable updates**: Only one external dependency to monitor and maintain - **Simplified auditing**: Security teams can easily review the entire dependency chain - **Faster installation**: Minimal dependencies result in quicker `npm install` times
### Cloudflare SDK (Stainless) - Complex dependency web ![Cloudflare SDK Dependencies](/assets/blog/speakeasy-vs-stainless/cloudflare-dependencies.png) The Cloudflare SDK includes a complex web of **25+ dependencies**, creating a more intricate dependency graph with: - **Larger attack surface**: More dependencies increase potential security risks - **Complex maintenance**: Multiple dependencies require ongoing monitoring for vulnerabilities - **Update complexity**: Changes in any dependency can introduce breaking changes - **Installation overhead**: More dependencies mean longer installation times
While the Cloudflare SDK's dependencies serve specific purposes (cross-platform compatibility, streaming support, etc.), all of that functionality can be achieved with minimal external dependencies as shown by the Speakeasy generated SDK. ## Generated types and type safety Both Speakeasy and Stainless generate TypeScript types, but they differ significantly in their approach to type safety. Speakeasy generates types with runtime validation using Zod, while Stainless relies primarily on compile-time TypeScript checking. Let's examine how each handles creating a deployment with project configuration:
```typescript filename="vercel-sdk-types.ts" // Vercel SDK (Speakeasy) - with runtime validation import { Vercel } from "@vercel/sdk"; const vercel = new Vercel({ bearerToken: "your-token" }); async function createDeployment() { // Strong typing with runtime validation const deployment = await vercel.deployments.createDeployment({ // !mark // !callout[/}/] Zod validates this at runtime and compile time name: "my-project", gitSource: { type: "github", repo: "user/repo", ref: "main" }, // Missing required fields will cause both // TypeScript errors AND runtime validation errors }); return deployment; } ```
```typescript filename="cloudflare-sdk-types.ts" // Cloudflare SDK (Stainless) - compile-time only import Cloudflare from "cloudflare"; const cf = new Cloudflare({ apiToken: "your-token" }); async function createZone() { // Strong compile-time typing, no runtime validation const params: Cloudflare.ZoneCreateParams = { // !mark // !callout[/}/] Only TypeScript compile-time checking name: "example.com", account: { id: "account-id" } // Invalid data might pass TypeScript but fail at runtime }; const zone = await cf.zones.create(params); return zone; } ```
The Vercel SDK generates Zod schemas that validate data both at compile time and runtime, catching type errors before they reach the server. The Cloudflare SDK provides excellent TypeScript types but relies on server-side validation to catch runtime errors. This difference becomes critical when dealing with dynamic data or complex validation rules that can't be fully expressed in TypeScript's type system. ### Runtime type checking Speakeasy creates SDKs that are type-safe from development to production. As our CEO wrote, [type safe is better than type faith](/post/type-safe-vs-type-faith). The Vercel SDK uses [Zod](https://zod.dev/) to validate data sent to and received from the server. This provides safer runtime code execution and helps developers catch errors before making API calls. Here's what happens when invalid data is passed to each SDK:
```typescript filename="vercel-runtime-validation.ts" // Vercel SDK (Speakeasy) - Runtime validation import { Vercel } from "@vercel/sdk"; const vercel = new Vercel({ bearerToken: "your-token" }); async function updateProject() { try { await vercel.projects.updateProject({ idOrName: "my-project", requestBody: { // !mark // !callout[/:/] Zod validation error: Expected string, received number name: 12345, // Invalid: should be string framework: "nextjs" } }); } catch (error) { // Catches validation error immediately // before making the HTTP request console.log("Validation failed:", error.message); } } ```
```typescript filename="cloudflare-no-validation.ts" // Cloudflare SDK (Stainless) - No runtime validation import Cloudflare from "cloudflare"; const cf = new Cloudflare({ apiToken: "your-token" }); async function updateZone() { try { const params: any = { // Bypassing TypeScript // !mark // !callout[/:/] No runtime validation - sent to server as-is name: 12345, // Invalid: should be string account: { id: "account-id" } }; await cf.zones.create(params); } catch (error) { // Only catches server-side validation errors // after the HTTP request is made console.log("Server error:", error.message); } } ```
The Vercel SDK catches type errors immediately with Zod validation, providing clear error messages before making HTTP requests. The Cloudflare SDK relies on server-side validation, meaning invalid data is sent over the network and errors are discovered later. This difference is crucial when dealing with dynamic data, user input, or complex validation rules that can't be fully expressed in TypeScript's type system. ## Authentication handling Both SDKs handle authentication, but with different approaches and levels of built-in sophistication. The Vercel SDK (generated by Speakeasy) uses bearer token authentication with simple configuration. Authentication is handled automatically for all requests, with no additional setup required. The Cloudflare SDK (generated by Stainless) uses API token authentication and provides comprehensive error handling with specific error classes for different authentication scenarios, including `AuthenticationError`, `PermissionDeniedError`, and other HTTP status-specific errors. When it comes to OAuth 2.0 support, Speakeasy-generated SDKs include built-in OAuth 2.0 client credentials handling with automatic token lifecycle management, retries, and error handling. This means the SDK automatically fetches, stores, and refreshes tokens as needed without requiring additional code. Stainless-generated SDKs typically require manual OAuth implementation when OAuth is needed, meaning developers must write additional code to handle token acquisition, storage, refresh logic, and error recovery. ## MCP server Both Speakeasy and Stainless allow you to generate an MCP server, which reuses your SDK methods, based on your API. Stainless generates the MCP server by default in the SDK package, which makes the SDK package larger. At Speakeasy, because we believe in the separation of concerns, we provide the MCP server and the SDK as separate options you can choose to generate or not. This keeps your SDK package lightweight if you don't need the server functionality. To generate an MCP server, select the `Model Context Protocol (MCP) Server` option when generating the SDK. ![Selecting MCP option](/assets/blog/speakeasy-vs-stainless/selecting-mcp-option.png) Let's compare the generated MCP server for both SDKs.
### Speakeasy
### Stainless
Speakeasy again focuses on simplicity, giving you a simpler file structure with only the parts you need for an MCP server. Stainless has a more complicated file structure, with more bloat. ### Usage Both platforms let you run MCP servers locally using the setup instructions in the READMEs. Speakeasy makes distribution easier than Stainless. It creates DXT files that users can drag and drop into Claude Desktop without having to edit configuration files manually. Via [Gram](https://getgram.ai) you also get a hosted page for your MCP server that makes it easy to integrate into other MCP clients. Speakeasy (also through Gram) further provides advanced functionality like curating toolsets for specific use cases, custom tools, prompt refining and production-grade hosting in one click. ## Webhooks support Speakeasy-generated SDKs provide built-in support for webhook validation, payload parsing, and delivery with automatic signature verification using HMAC or other signing algorithms. The generated SDK includes strongly-typed webhook event handlers based on the OpenAPI specification. The Vercel SDK includes webhook handling capabilities as part of its comprehensive feature set, allowing developers to validate and process webhook events with minimal setup. Stainless-generated SDKs typically don't provide out-of-the-box webhook functionality. The Cloudflare SDK focuses primarily on API calls rather than webhook handling, requiring developers to implement their own logic for verifying event signatures, defining event payload types, and managing webhook delivery mechanisms. This difference means that APIs requiring webhook support benefit significantly from Speakeasy's built-in webhook validation and type-safe event handling, while Stainless users must build these features manually. You can read more about how Speakeasy handles webhooks in our [webhooks release post](/post/release-webhooks-support). ## React Hooks React Hooks simplify state and data management in React apps, enabling developers to consume APIs more efficiently. Speakeasy generates built-in React Hooks using [TanStack Query](https://tanstack.com/query/latest). These hooks provide features like intelligent caching, type safety, pagination, and integration with modern React patterns like SSR and Suspense. Stainless does not generate React Hooks. ```tsx filename="speakeasy-example/booksView.tsx" import { useQuery } from "@tanstack/react-query"; function BookShelf() { // loads books from an API const { data, status, error } = useQuery( [ "books", // Cache key for the query ], async () => { const response = await fetch("https://api.example.com/books"); return response.json(); }, ); if (status === "loading") return

Loading books...

; if (status === "error") return

Error: {error?.message}

; return (
    {data.map((book) => (
  • {book.title}
  • ))}
); } ``` For example, in this basic implementation, the `useQuery` hook fetches data from an API endpoint. The cache key ensures unique identification of the query. The `status` variable provides the current state of the query: `loading`, `error`, or `success`. Depending on the query status, the component renders `loading`, `error`, or the fetched data as a list. For an in-depth look at how Speakeasy uses React Hooks, see our [official release article](/post/release-react-hooks). ## Summary We've all experienced bad SDKs that make integration with the API harder, not easier. Speakeasy is building a generator to make poorly written, poorly maintained SDKs a thing of the past. To do so, our team has put an extraordinary level of thought into getting the details of SDK generation right. We think that the effort has earned us the position to compare favorably with any other generator. If you are interested in seeing how Speakeasy stacks up against some of the popular open-source SDK-generation tools, check out [this post](/post/compare-speakeasy-open-source). # standalone-functions Source: https://speakeasy.com/blog/standalone-functions import { CodeWithTabs } from "@/mdx/components"; import { Callout } from "@/mdx/components"; Today we're introducing a feature for every Speakeasy-generated TypeScript SDK, called Standalone Functions. This feature makes it possible for your users to build leaner apps on top of your API, that can run in the browser, or any other environment where performance is an important consideration. ## The short version Rather than needing to import the entire library, your SDK users will be able to select specific functions they want to use. Here's what the change looks like with [Dub's SDK](https://dub.co/): ```ts import { Dub } from "dub"; async function run() { const dub = new Dub(); const count = await dub.links.count(); console.log("Link count:", count); } run(); ``` Into this: ```ts import { DubCore } from "dub/core.js"; import { linksCount } from "dub/funcs/linksCount.js"; async function run() { const dub = new DubCore(); const result = await linksCount(dub); if (!result.ok) { throw result.error; } // Use the result console.log("Link count:", result.value); } run(); ``` All the SDK's unused functionality: methods, Zod schemas, encoding/decoding helpers and so on, will be excluded from the user's final application build. The bundler will tree-shake the package and handle minification of the unused code from within the modules. Additionally, standalone functions return a `Result` type which brings about better error handling ergonomics. Standalone functions _do not_ replace the existing class-based interface. All Speakeasy TypeScript SDKs now provide both functions and classes. We think they are both equally valid ways for developers to work with SDKs. The decision on which style to use can be based on the project you're building and what constraints are in place. ## Impact Combining standalone functions with ES Modules yields massive savings as shown in the bundle analysis for the sample programs above. Using `dub@0.35.0` with the class-based API: ![Bundle size of about 324.4 kilobytes when using Dub v0.35.0 with the class-based API](/assets/blog/standalone-functions/dub-0.35.0-impact.jpeg) Using `dub@0.36.0` with the standalone functions API: ![Bundle size of about 82.1 kilobytes when using Dub v0.36.0 with the standalone functions API](/assets/blog/standalone-functions/dub-0.36.0-impact.jpeg) Please note that the sizes above are _before_ compressing the bundles with Gzip or Brotli. The uncompressed size is a good proxy for metrics like JavaScript parsing time in the browser. Regardless, it's a great idea to compress code before serving it to browsers or using a CDN that handles this for you. Our TypeScript SDKs use Zod to build great support for complex validation and (de)serialization logic. If you are already using Zod in your application then consider excluding its size (~50KB) when thinking about the numbers above. The impact is even more compelling in this case. ## The long version Many SDKs are built around the concept of a class that you instantiate and, through it, access various operations: ```ts import { Beezy } from "@speakeasy-sdks/beezyai"; const beezy = new Beezy(); // The SDK class const stream = await beezy.chat.stream({ prompt: "What is the most consumed type of cheese?", model: "ex-30b", }); // Use the result... ``` This works great from a developer experience perspective especially if you are using an IDE with language server support. You can autocomplete your way to most functionality and breeze through your work. If we dig under the surface, we would likely find a typical SDK is arranged like so: > { /* ... */ } /** * Generate a complete response to a prompt. */ async complete(req: CompletionRequest): Promise { /* ... */ } }`, }, { label: "src/files.ts", language: "typescript", code: `import { encodeMultipart } from "./lib/encodings.js" class Files extends APIClient { /** * Create a multipart request to upload files for fine tuning and other jobs. */ async upload(req: FileUploadRequest): Promise { /* ... */ } }`, }, { label: "src/chat-history.ts", language: "typescript", code: `import { createPaginatedIterable } from "./lib/pagination.js" import { encodeJSON } from "./lib/encodings.js" class ChatHistory extends APIClient { /** * Paginated list of chat sessions. */ async function list(req: ListChatsRequest): Promise { /* ... */ } }`, } ]} /> However, if you're building a web app/site, there is a downside to this approach that is not immediately apparent: the entire SDK will be included in your app's bundle because there is little or no opportunity to tree-shake or exclude unused code from the SDK. If we were to bundle the snippet of code above, then all the code in the `Beezy`, `Chat`, `Files` and `ChatHistory` classes as well as all of the code that supports those classes an their methods, such as pagination and multipart request helpers, will be include in our app. Yet, we only called `beezy.chat.stream()`. ### A brief crash course on bundlers and tree-shaking A lot of web apps are built using front-end frameworks with bundlers such as [Rollup][rollup], [Webpack][webpack], [ESBuild][esbuild], [Turbopack][turbopack]. Bundlers are responsible for a bunch of tasks including taking your TypeScript code and code from all the libraries you used, converting into a JavaScript files that can be loaded on the browsers. They also employ techniques to reduce the amount of JavaScript to load such as [minifying][zod-minify] the code, splitting a bundle into smaller parts to load functionality incrementally and tree-shaking to eliminate unused code from your codebase and the libraries you've used. [rollup]: https://rollupjs.org/ [webpack]: https://webpack.js.org/ [esbuild]: https://esbuild.github.io/ [turbopack]: https://turbo.build/pack/docs [zod-minify]: /post/writing-zod-code-that-minifies Tree-shaking is the process of identifying what parts of a JavaScript module were used and only including that subsection of the module. Here's an example from ESBuild: ![Example of bundling a simple app with ESBuild](/assets/esbuild-bundle-example.jpeg) ([Playgound link for the screenshot above][esbuild-playground]) [esbuild-playground]: https://esbuild.github.io/try/#YgAwLjIzLjAALS1idW5kbGUgLS1mb3JtYXQ9ZXNtAGUAZW50cnkuanMAaW1wb3J0IHsgZ3JlZXQgfSBmcm9tICIuL21vZHVsZS1hLmpzIjsKCmNvbnNvbGUubG9nKGdyZWV0KCJHZW9yZ2VzIikpOwAAbW9kdWxlLWEuanMAZXhwb3J0IGZ1bmN0aW9uIGdyZWV0KG5hbWUpIHsKICByZXR1cm4gYEhlbGxvLCAke25hbWV9IWA7Cn0KCmV4cG9ydCBmdW5jdGlvbiBhZGQoYSwgYikgewogIHJldHVybiBhICsgYjsKfQoKZXhwb3J0IGNvbnN0IFBJID0gMy4xNDsKCmV4cG9ydCBjb25zdCBjb2xvcnMgPSB7CiAgcmVkOiAiI2ZmMDAwMCIsCiAgZ3JlZW46ICIjMDBmZjAwIiwKICBibHVlOiAiIzAwMDBmZiIsCn07 Notice how in the build output, the `add`, `PI` and `colors` exports were not included. Most bundlers are capable of tree-shaking, some apply better or more heuristics than others, but generally the rule is to analyze which module exports were used and leave out the rest. ### And we're back So if we understand how tree-shaking works, we can arrange our SDK code a little differently and greatly reduce the impact of our package on a web app's total bundle size. This is what's new in our recent changes to the generator. We now create a folder in every TypeScript SDK at `src/funcs/` and emit standalone functions. Here's a simplified example of one: ```typescript import { BeezyCore } from "../core.js"; export async function chatStream( client: BeezyCore, req: ChatRequest, ): Promise< Result< EventStream, ForbiddenError | RateLimitError | TimeoutError > > { /* ... */ } ``` The interesting change is that instead of attaching methods to classes, we designed functions to take a tiny "client" as their first argument. This small _inversion_ means bundlers can dial it up to the max with their tree-shaking algorithms since functions are module-level exports. ## Playing the long game When you peer into a function's source code today, you'll notice it's more verbose than a one line call to a massive HTTP client abstraction. There's code to validate data with Zod schemas, encode query, path and header parameters and execute response matching logic. This was a deliberate decision because it allows us, and by extension you, to have fine-grained control over tree-shaking. Whereas deep abstractions are very appealing at first, they end up unnecessarily dragging in all the functionality an SDK provides even if small subsets of it are needed. We're choosing shallower abstractions instead and reaping the benefits. From the results we've seen so far, we think standalone functions are the right building block for modern web apps. We're excited to see what you'll build with them. # streamlined-sdk-testing-ai-ready-apis-with-mcp-server-generation Source: https://speakeasy.com/blog/streamlined-sdk-testing-ai-ready-apis-with-mcp-server-generation import { Callout, ReactPlayer } from "@/lib/mdx/components"; We've made SDK Testing more powerful with real server validation, GitHub integration, and multi-operation workflows. Plus, every TypeScript SDK now includes an MCP server, making your API instantly accessible to AI-powered tools like Claude, Cursor, Zed and many other AI applications. Let's dive into what's new! ## 🧪 More Powerful SDK Testing We've enhanced our SDK Testing capabilities to help API providers maintain reliable client libraries with less effort. Our revamped SDK Testing now provides comprehensive validation for SDKs in Go, TypeScript, and Python. ### ✨ Key Highlights - **Autogenerated tests from OpenAPI specs** with rich field coverage based on your schema definitions - **Zero-config mock servers** that simulate your API behavior without connecting to backend systems - **Real server testing** for validating against actual API endpoints - **Multi-operation test workflows** using the Arazzo specification for complex scenarios - **GitHub Actions integration** for continuous validation on every PR 🔗 [**Read the Full SDK Testing Release Post**](https://www.speakeasy.com/post/release-sdk-testing) for in-depth details on implementation, examples, and advanced usage. ### ⚡ Getting Started in Three Simple Steps 1️⃣ **Generate tests** with `speakeasy configure tests`
2️⃣ **Run tests locally or in CI/CD** with `speakeasy test`
3️⃣ **View reports** in the Speakeasy dashboard for insights. ![Test reports](/assets/changelog/assets/changelog-2025-03-06-dashboard.png) 🔗 [**Read the full SDK Testing release post**](https://www.speakeasy.com/post/release-sdk-testing) for in-depth details on implementation, examples, and advanced usage. --- ## 🤖 MCP Server Generation: AI-Ready APIs, No Extra Work Two weeks ago, we launched MCP Server Generation, and the response has been incredible! We've seen strong adoption across the community, with great feedback from developers who have integrated MCP servers into their APIs. ### What is MCP? Model Context Protocol (MCP) is an open standard from Anthropic that enables secure, two-way connections between APIs and AI-powered tools like Claude, Cursor, and Zed. Think of MCP as a Rosetta Stone for your APIs—allowing AI agents to instantly understand and interact with your services without custom integrations or deciphering complex documentation. ### 🚀 What's New? Every TypeScript SDK generated by Speakeasy now includes an MCP server that: - **Acts as a thin wrapper** around your existing TypeScript SDK - **Generates a tool for each SDK method** so AI agents can discover and use them - **Uses your SDK's Zod schemas** to give agents an accurate picture of request formats - **Supports customization** via OpenAPI extensions to control tool names and descriptions - **Includes scoping capabilities** to tag and control which operations are available to AI agents The generated MCP server structure looks like this: ```yaml filename="bluesky-ts/src/mcp-server" ├── tools │ ├── accountDelete.ts │ ├── accountExportData.ts │ ├── accountsGetInviteCodes.ts │ ├── actorGetSuggestions.ts │ └── ... ├── build.mts ├── mcp-server.ts ├── resources.ts ├── server.ts ├── shared.ts └── tools.ts ``` Watch Claude using Dub's MCP server to create a shortlink in real-time:
### ⚡ Getting Started With MCP servers now included in every TypeScript SDK: - **AI agents can instantly interact** with your API - **No extra setup is needed**-just merge the PR and run the server - **Launch with scopes** to control which operations are available: ``` npx your-sdk mcp start --scope read ``` ### 📢 Customers Already Using MCP Servers Shoutout to [**Dub**](https://github.com/dubinc/dub-ts)**,** [**Polar**](https://github.com/polarsource/polar-js)**,** [**Hathora**](https://github.com/hathora/cloud-sdk-typescript)**,** [**Novu**](https://github.com/novuhq/novu-ts)**,** [**Acuvity**](https://github.com/acuvity/acuvity-ts) for being among the first to generate their MCP servers! 🚀 🔗 [**Learn More About MCP Server Generation**](/docs/standalone-mcp/build-server) 🔗 [**Read the Full Release Post**](https://www.speakeasy.com/post/release-model-context-protocol) --- ## 🛠️ New Features and Bug Fixes 🐛 Based on the most recent CLI version: [**Speakeasy v1.510.0**](https://github.com/speakeasy-api/speakeasy/releases/tag/v1.510.0) **Platform**\ 🐛 **Fix:** Prevented compilation errors with naming in various targets due to special characters in operationId.\ 🐝 **Feat:** Improved inline schema naming to refine name resolution and reduce ambiguities.\ 🐝 **Feat:** Added --env CLI flag for passing environment variables to MCP servers.\ 🐝 **Feat:** Improved security and global authentication handling in MCP servers. **Python**\ 🐝 **Feat:** Implemented TYPE_CHECKING safeguards to properly handle circular dependencies in Pydantic models. **Ruby**\ 🐛 **Fix:** Removed extraneous config_server and config_server_url methods in SDK class.\ 🐝 **Feat:** Added global security flattening to streamline authentication via constructor parameters.\ 🐝 **Feat:** Refactored Ruby serializer into a standalone module for better maintainability. **TypeScript**\ 🐝 **Feat:** Switched to jsonpath-rfc9535 for RFC9535-compliant JSONPath handling, improving compatibility with Next.js and SWC.\ 🐝 **Feat:** Introduced Model Context Protocol (MCP) servers for hosting SDK functions as callable tools. **Terraform**\ 🐝 **Feat:** Added support for **map of array types** (e.g., map of list of strings).\ 🐝 **Feat:** Enabled proper rendering of **array of array types** (e.g., list of list of floats).\ 🐝 **Feat:** Added support for **array of empty objects** to improve Terraform schema handling.\ 🐝 **Feat:** Enabled support for **maps of double, float, int32, and number types**.\ 🐛 **Fix:** Properly rendered union type examples with stable single selection.\ 🐛 **Fix:** Prevented OAuth authentication errors when other API security options are available.\ 🐛 **Fix:** Prevented package name shadowing for required dependencies.\ 🐛 **Fix:** Correctly rendered examples for maps of booleans and integers.\ 🐛 **Fix:** Added support for nullable maps in Terraform SDKs. # tags-best-practices-in-openapi Source: https://speakeasy.com/blog/tags-best-practices-in-openapi import { Callout } from "@/mdx/components"; Hi! These blog posts have been popular, so we've built an entire [OpenAPI Reference Guide](/openapi/) to answer any question you have. It includes detailed information on [**tags**](/openapi/tags). Happy Spec Writing! ## Introduction This article explains how to use [tags in OpenAPI](https://spec.openapis.org/oas/latest.html#tag-object). Tags are the way to organize your API endpoints into groups to make them easier for users to understand. ## Definitions Every separate feature in your API that a customer can call is named an **endpoint**. An endpoint is an **operation** (an HTTP method like GET or POST) applied to a **path** (for example, `/users`). Below is an example endpoint in an OpenAPI schema. The path is `play`, and the operation is a POST. ```yaml paths: /play: post: description: Choose your jazz style. operationId: band#play ``` In informal conversation, people often refer to API endpoints and operations as if the terms are interchangeable. Paths are the natural way to categorize your endpoints. You might have separate paths in your API for products, purchases, or accounts. However, paths are hierarchical. Each endpoint can be in only one path. This may make it difficult for users of your API to browse to the feature they are looking for if they assume it's under a different path. By contrast, each endpoint may have multiple tags, so may be shown in multiple groups in your schema documentation. ## Example Schema To demonstrate using tags, let's make a simple schema for a jazz club API with just three paths: - `/play` — plays music. - `/stop` — stops music. - `/order` — orders a drink. ### API Information Start with some basic information about your API. ```yaml openapi: 3.0.3 info: title: The Speakeasy Club description: A club that serves drinks and plays jazz. version: 1.0.0 servers: [] ``` ### Simple Tag Add the `tags` root-level object with a tag for drink operations. Only the tag `name` is mandatory. ```yaml openapi: 3.0.3 info: title: The Speakeasy Club description: A club that serves drinks and plays jazz. version: 1.0.0 servers: [] tags: - name: Drinks ``` ### Detailed Tag Add another tag with more detail. The **Music** tag has a `description` string and an `externalDocs` object with two required fields: `description` and `url`. The URL points to information anywhere on the web that you want to use to describe the endpoint. Use `externalDocs` if you don't want to overcrowd your schema with unnecessary detail or if another department in your company maintains the documentation separately. ```yaml openapi: 3.0.3 info: title: The Speakeasy Club description: A club that serves drinks and plays jazz. version: 1.0.0 servers: [] tags: - name: Drinks - name: Music description: A band that plays jazz. externalDocs: description: List of jazz genres url: https://en.wikipedia.org/wiki/List_of_jazz_genres ``` ### Paths Now that we have tag definitions, we can tag our endpoints. Here the `/play` and `/stop` endpoints are tagged with `Music`, and the `/order` endpoint is tagged with `Drinks`. We could also make another tag called `Front of house` and apply it to both endpoints to organize them separately to `Backstage` endpoints. ```yaml openapi: 3.0.3 info: title: The Speakeasy Club description: A club that serves drinks and plays jazz. version: 1.0.0 servers: [] tags: - name: Drinks - name: Music description: A band that plays jazz. externalDocs: description: List of jazz genres url: https://en.wikipedia.org/wiki/List_of_jazz_genres paths: /play: post: tags: - Music summary: Play music description: Choose your jazz style. operationId: band#play requestBody: required: true content: application/json: schema: $ref: "#/components/schemas/PlayRequestBody" example: style: Bebop responses: "204": description: No Content response. /stop: post: tags: - Music summary: Stop music description: Stop playing. operationId: band#stop responses: "204": description: No Content response. /order: post: tags: - Drinks summary: Order tea description: Order a cup of tea. operationId: order#tea requestBody: required: true content: application/json: schema: $ref: "#/components/schemas/TeaRequestBody" example: includeMilk: false responses: "204": description: No Content response. ``` ## The Full Schema Example Below is the full example schema with `components` added to specify how to call the endpoints. Paste the code into the [Swagger editor](https://editor.swagger.io/) to see it displayed as a formatted document. Note that operations in the Swagger output are grouped by `tag`. ```yaml openapi: 3.0.3 info: title: The Speakeasy Club description: A club that serves drinks and plays jazz. version: 1.0.0 servers: [] tags: - name: Drinks - name: Music description: A band that plays jazz. externalDocs: description: List of jazz genres url: https://en.wikipedia.org/wiki/List_of_jazz_genres paths: /play: post: tags: - Music summary: Play music description: Choose your jazz style. operationId: band#play requestBody: required: true content: application/json: schema: $ref: "#/components/schemas/PlayRequestBody" example: style: Bebop responses: "204": description: No Content response. /stop: post: tags: - Music summary: Stop music description: Stop playing. operationId: band#stop responses: "204": description: No Content response. /order: post: tags: - Drinks summary: Order tea description: Order a cup of tea. operationId: order#tea requestBody: required: true content: application/json: schema: $ref: "#/components/schemas/TeaRequestBody" example: includeMilk: false responses: "204": description: No Content response. components: schemas: PlayRequestBody: type: object properties: style: type: string description: Style of music to play example: Bebop enum: - Bebop - Swing example: style: Bebop required: - style TeaRequestBody: type: object properties: includeMilk: type: boolean description: Whether to have milk. example: true example: includeMilk: false ``` Below is what it looks like in the editor. ![Swagger output](/assets/blog/tags-best-practices-in-openapi/swagger_tag_example.png) ## Tags in Speakeasy Speakeasy will split the SDKs and documentation it generates based on your tags. You can add the [x-speakeasy-group](/docs/customize-sdks/namespaces#define-namespaces-without-tags) field to an endpoint to tell Speakeasy to ignore the endpoint's tag and group it under the custom group instead. ## Conclusion That's everything you need to know about tags in OpenAPI. There are just three more tag rules you might want to know: - Tags are optional, both at the root level and on endpoints. - Tags must have unique names in your schema. - The tag `description` may use [CommonMark syntax](https://spec.commonmark.org/). # terraform-enhancing-validation Source: https://speakeasy.com/blog/terraform-enhancing-validation When working with APIs, ensuring that the values we send meet specific requirements, such as string lengths or numerical ranges, is crucial. This validation becomes even more critical when using Terraform Providers to interact with these APIs. Relying only on server-side validation can lead to slow feedback loops, errors during the apply phase, and frustrated users. Plan validation in Terraform Providers helps catch these issues earlier, improving the end-user experience by providing immediate feedback and preventing errors before they occur. This blog post explores how you can add configuration validation to your Terraform Providers. ### The Problem with Missing Validation APIs often enforce strict value requirements, such as minimum and maximum string lengths, numerical ranges, or specific formats. When a Terraform Provider interacts with an API but lacks validation for these requirements, Terraform's validate and plan operations may proceed without issues. However, the apply operation can fail due to API errors, leading to frustrating and time-consuming feedback loops. These failures can occur in the middle of applying resources, causing further complications and delays. Imagine you're deploying multiple resources, and halfway through the process, an API error occurs because of an invalid string length. You would need to fix the issue and rerun the apply operation, wasting valuable time and resources. The goal is to enhance the user experience by having Terraform raise validation errors before applying configurations, ensuring smoother deployments and reducing the risk of mid-apply failures. ### Why Does It Matter? - **Efficiency**: Early error detection saves time and resources by preventing mid-apply failures. - **User Experience**: Immediate feedback during validation enhances satisfaction and reduces frustration. - **Resource Management**: Preventing mid-apply failures ensures infrastructure remains stable and consistent. - **Scalability**: Early validation maintains a reliable deployment process as infrastructure grows. - **Error Reduction**: Automating validation reduces the risk of human error and ensures consistent application of validation rules. ### Manual Validation in Terraform Providers Adding validation to Terraform Providers involves implementing [validators](https://developer.hashicorp.com/terraform/plugin/framework/validation) within the resource schema. Let's assume you are developing a Terraform provider for managing a simple resource, such as a "User" resource, which has a "username" attribute: ```go func NewUserResource() resource.Resource { return &userResource{} } type userResource struct{} func (r *userResource) Metadata(_ context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { resp.TypeName = req.ProviderTypeName + "_user" } func (r *userResource) Schema(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) { resp.Schema = schema.Schema{ Attributes: map[string]schema.Attribute{ "username": stringattribute.String{ Required: true, Description: "The username of the user.", Validators: []validator.String{ stringvalidator.LengthBetween(6, 64), stringvalidator.RegexMatches( regexp.MustCompile(`^[a-z]+$`), "must contain only lowercase alphanumeric characters", ), }, }, }, } } ``` - The `username` attribute is defined with type `schema.TypeString` and marked as required. - The `username` attribute is defined with string validation helpers from [`terraform-plugin-framework-validators`](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-framework-validators) that checks if the length of the string is between 6 and 64 characters, as well as only lowercase characters. Consider the following configuration: ``` resource "user" "test" { username = "abcd" } ``` Running `terraform validate` will produce an error if the `username` length does not meet the specified criteria: ```go - Error: Invalid value for field "username": String length must be between 6 and 64 characters. - Error: Invalid value for field "username": must contain only lowercase alphabetic characters. ``` [See more in the terraform-plugin-framework-validators package here](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-framework-validators). Repeating this process for every field in your Terraform Provider is daunting, but necessary to enhance the user experience by catching errors early in the validation phase, preventing issues during the apply phase, and ensuring smoother deployments. ### Configuration Validation with Speakeasy Speakeasy simplifies the process of adding configuration validation to Terraform Providers by automatically generating validation handlers based on your OpenAPI specification. By default, these OpenAPI specification properties are automatically handled: - For `string` types: `enum`, `maxLength`,`minLength`, and `pattern` - For integer types: `enum`, `minimum` and `maximum` - For `array` types: `maxItems`, `minItems`, and `uniqueItems` For scenarios not covered by these default handlers, Speakeasy supports custom validation logic. If you're interested in finding out more, see our [Terraform Provider generation documentation](/docs/create-terraform) and join our [Slack](https://go.speakeasy.com/slack) to chat with our engineering team. ### Conclusion: Enhancing Terraform Providers with Validation Adding configuration validation to Terraform Providers is essential for improving the end user experience and ensuring smooth deployments. By implementing validation, whether manually or through generated providers like those created by Speakeasy, developers can ensure consistent, efficient, and reliable configurations, ultimately benefiting API consumers. With robust validation in place, the risk of errors is minimized, leading to more stable and predictable infrastructure management. # terraform-generation-alpha Source: https://speakeasy.com/blog/terraform-generation-alpha If you are interested in being an alpha user for our Terraform provider generator, please respond to this email to let us know! Github's 2022 annual review crowned HCL (hashicorp configuration language) as the fastest growing language (56%), even beating out developer darling, Rust, for the top spot. It is a testament to the establishment of infrastructure as code (IaC) as a leading development practice and the popularity of Terraform as the medium for infrastructure management. The best developer companies meet their users where they already are, to flatten the learning curve and provide a great DevEx. For some API companies that increasingly means a terraform provider so that the resources exposed by the API can be managed in concert with any related infrastructure. However, like SDKs, a terraform provider becomes another artifact that needs ongoing investment & management. That's why we've been working on a way to enable teams to autogenerate terraform providers from their OpenAPI definition. ## Alpha Release **Terraform Provider Generation** - Just add annotations to your OpenAPI specification's entities and operations. Speakeasy will then process your spec to produce and maintain a terraform-registry compliant provider plugin, that will create, update and destroy persistent resources by interacting with your APIs. As an example, imagine the pet entity from the canonical petshop example: ```yaml paths: /pet: post: tags: - pet summary: Add a new pet to the store x-speakeasy-entity-operation: Pet#create description: Add a new pet to the store operationId: addPet responses: '200': description: Successful operation content: application/json: schema: $ref: '#/components/schemas/Pet' requestBody: description: Create a new pet in the store required: true content: application/json: schema: $ref: '#/components/schemas/Pet' … Pet: x-speakeasy-entity: Pet required: - name - photoUrls properties: id: type: integer format: int64 example: 10 name: type: string category: $ref: '#/components/schemas/Category' photoUrls: type: array items: type: string … ``` Adding the `x-speakeasy-entity` annotation to a resource-based API, along with annotations to each operation such as: - `x-speakeasy-entity-operation: Pet#create` - `x-speakeasy-entity-operation: Pet#delete` - `x-speakeasy-entity-operation: Pet#update` are all that's required to generate a valid terraform provider with terraform-registry documentation, usable like the following: ```HCL resource "petstore_pet" "myPet" { name = "Freddie" photo_urls = ["https://example.com/example.jpg"] tags = { name = "foo" } } ``` Speakeasy will output this provider plugin to a github repository, annotating resources with Computed, ReadOnly, Required and ForceNew attributes based upon the semantics of how they're used in Create/Update operations. If you would like us to generate a terraform provider from your OpenAPI definition, please get in touch! We're actively looking for design partners for whom we can generate/maintain terraform providers for your API. ## New Features **SDK Retries** - Every SDK should have retries. It's one of the things that makes for a great DevEx. End-users shouldn't be left to figure out what errors should trigger retries, which shouldn't, and how long you should wait before resending. Nobody knows an API better than the builder, and that's who should be determining when a retry is appropriate. That's why we've added the ability for our users to extend their OpenAPI spec and configure retry logic for their SDKs. ```yaml x-speakeasy-retries: strategy: backoff backoff: initialInterval: 100 # 100 ms maxInterval: 30000 # 30s exponent: 2 # backoff delay doubles from 100ms until 30s maxElapsedTime: 300000 # After 5 minutes, returns a failure to the callee statusCodes: # a list of status codes to retry on (supports XX wildcards) - 429 - 5XX retryConnectionErrors: true # whether to retry connection errors ``` # terraform-v2-cli-upgrade Source: https://speakeasy.com/blog/terraform-v2-cli-upgrade Following on from our TypeScript work in January, we've kept our focus on upleveling our developer experience of both our platform and our generation targets. This month our CLI got an overhaul, and Terraform got a package of upgrades. The result is an experience that is more intuitive, more powerful, and more flexible for both our users and their end users. Let's jump into it 🚀 ## Terraform v2 Terraform is the go to tool for businesses implementing a strict disaster recovery plan. For any SaaS businesses trying to sell API usage to these enterprises, that makes offering a Terraform Provider a must-have. Our Terraform generation has made maintaining a provider trivially easy and unlocked our customers ability to win large enterprise contracts. We've been working hard to extend that opportunity to every API. That's why we're excited to announce the release of Terraform v2 with a range of new features to support increasingly complex APIs: - Collapse multiple API endpoints into a single Terraform Entity. - Add Custom Plan Validators to your Provider - Automatically enforced Runtime Validations - Support for `default` and `const` attributes - Handling of Batch Endpoints For the details, please read the full release notes [here](/post/release-terraform-v2). ## Upgraded CLI - Enhanced Quickstart We received a lot of feedback from our users about the CLI being their preferred interface for generating SDKs. We've therefore invested in creating what we think is one of the best CLI experiences available anywhere today. The principles that guided our rebuild were: - ✨ No magic incantations required - 😀 Flattened command hierarchy - 👩‍🔧 Design for humans Tangibly it means you don't need any context or docs to use the CLI. Our interactive prompts will make setting up your SDKs a breeze.
If you're interested in learning how we built the new CLI, [the code is open source](https://github.com/speakeasy-api/speakeasy) and a detailed blog post is available [here](/post/how-we-built-cli). ## 🚢 Improvements and Bug Fixes 🐛 #### Most recent version: [Speakeasy v1.200.1](https://github.com/speakeasy-api/speakeasy/releases/tag/v1.200.1) 🚢 Generation of SSE usage examples in `Readme.md`\ 🐛 Correct construction of input statements when readOnly: true\ 🚢 use defaults for usage snippet rendering if example not available\ 🚢 support disabling security for a single operation\ 🐛 don't regenerate if no version bump detected\ 🐛 handle nullable enums correctly\ ### Typescript 🚢 Support for [Retry-After](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Retry-After) headers\ 🚢 Support for URL based cursor based pagination\ 🚢 Allow string enums to be used in SSE data\ 🚢 Make const fields optional on the client\ 🚢 Improve validation errors thrown from SDK methods\ 🚢 Introduce new response format with http metadata\ 🚢 Support SDKs that do not have global servers ### Java 🚢 Support for new Sonatype Central Publishing\ 🚢 Support bigint, decimal format\ 🚢 Retries support\ 🐛 Handle non-numeric cursors for pagination\ 🐛 Fixed imports and handling of security builder field names\ 🐛 Pagination response constructor signature, internal whitespace/ordering ### Python 🐛 Invalid dataclasses if method has params both with and without default value ### Ruby 🐛 fix server param in docstring\ 🐛 handle named servers\ 🐛 improve security and parameter handling\ 🐛 improve unmarshalling of complex data types\ 🐛 change base64 encodingtechnique to support long credentials ### Terraform 🚢 Allow for arrays of non-string enums\ 🚢 Support for custom plan validators ### C# 🚢 example generation for complex objects\ 🚢 add support for nullable request bodies\ 🚢 support .NET5 compilation checks\ 🚢 extend BigInteger/Decimal support to arrays and maps\ 🚢 add support for global security callbacks\ 🐛 revert dotnet version default to 5.x ### Go 🚢 Allow string enums to be used in SSE data # test-suite-generation-early-access Source: https://speakeasy.com/blog/test-suite-generation-early-access import { Callout, CodeWithTabs } from "@/lib/mdx/components"; If you are interested in participating in early access, please fill out [this form](https://speakeasyapi.typeform.com/testing-early), or, if you're an existing customer, drop a note in your Speakeasy Slack connect channel. If at first you don't succeed test, test, test again. We're excited to announce the early access release of our new Test Generation feature. It empowers developer teams to ship SDKs with confidence that they work as intended for end users. ## Test Generation { const beezy = new index_js_1.Beezy({ security: { clientID: process.env("BEEZY_CLIENT_ID"), clientSecret: process.env("BEEZY_CLIENT_SECRET"), }, }); const result = await beezy.analysis.analyzeText({ text: "What is the difference between OpenAPI and Swagger?", analysisTypes: ["keywords"], model: "ex-7b", }); (0, vitest_1.expect)(result).toEqual({ results: [ { keywords: [""], }, ], }); });`, }, { label: "test_analysis.py", language: "python", code: `from beezyai import Beezy from beezyai.models import shared def test_analysis_analyze_text(): s = Beezy( security=shared.Security( client_id="", client_secret="", ), ) assert s is not None res = s.analysis.analyze_text(request={ "text": "What is the difference between OpenAPI and Swagger?", "analysis_types": [ shared.AnalysisTypes.KEYWORDS, ], "model": shared.Model.EX_7B, }) assert res is not None assert res is not None assert res == shared.TextAnalysisResponse( results=[ shared.KeywordAnalysis( keywords=[ "", ], ), ], )`, }, { label: "analysis_test.go", language: "go", code: `package tests import ( "beezyai" "beezyai/models/shared" "context" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "testing" ) func TestAnalysis_AnalyzeText(t *testing.T) { s := beezyai.New( beezyai.WithSecurity(shared.Security{ ClientID: "", ClientSecret: "", }), ) var text string = "What is the difference between OpenAPI and Swagger?" analysisTypes := []shared.AnalysisTypes{ shared.AnalysisTypesKeywords, } var model *shared.Model = shared.ModelEx7b.ToPointer() ctx := context.Background() res, err := s.Analysis.AnalyzeText(ctx, text, analysisTypes, model) require.NoError(t, err) require.NotNil(t, res) assert.Equal(t, shared.TextAnalysisResponse{ Results: []shared.Results{ shared.CreateResultsKeywordAnalysis( shared.KeywordAnalysis{ Keywords: []string{ "", }, }, ), }, }, *res.TextAnalysisResponse) }`, } ]} /> ### What's Included - Support for TypeScript, Python, and Go, - Test creation based on the examples in your OpenAPI spec, - The ability to specify custom requests and responses for your tests. ### How It Works 1. Specify the examples you want to use in your OpenAPI spec. 2. Optionally specify custom examples via a `tests.yaml`. 3. Speakeasy generates tests for your SDKs based on the examples you provided. The generated tests will be created in a new `tests` directory in your SDK. For each language, we've selected a popular testing framework for the generated tests: - TypeScript: [Vitest](https://vitest.dev/) - Python: [Pytest](https://docs.pytest.org/en/stable/) - Go: [Testify](https://github.com/stretchr/testify) along with Go's built-in testing package --- ## 🐝 New Features and Bug Fixes 🐛 Based on the most recent CLI version: [**Speakeasy v1.353.1**](https://github.com/speakeasy-api/openapi-generation/releases/tag/v1.353.1) ### TypeScript 🐝 Feat: Readme's render with environment variable usage \ 🐛 Fix: Updated `.gitignore` rules for TS SDKs ### Python 🐝 Feat: Readme's render with environment variable usage \ 🐝 Feat: Added Python debug logger interface \ 🐛 Fix: Buffering of streaming responses and TypedDict unmarshalling in pythonv2 \ 🐛 Fix: Handle `consts` in non-json ### Go 🐝 Feat: Readme's render with environment variable usage ### Terraform 🐛 Fix: Dependencies upgraded \ 🐛 Fix: Prevent creating attribute validators for invalid or RE2 engine incompatible OAS pattern \ 🐛 Fix: Support for flattened security env vars ### Java 🐝 Feat: Support for discriminated oneOf ### C# 🐝 Feat: Implemented support for `x-speakeasy-enums` # the-rest-template-project Source: https://speakeasy.com/blog/the-rest-template-project ## Github Repository: [speakeasy-api/rest-template-go](https://github.com/speakeasy-api/rest-template-go) Building a RESTful API can be daunting for developers who have never done it before (and even those who have). There are a number of choices and best practices that need to be considered as part of an API's implementation; it can be hard to know where to start. That's why we're pleased to announce the [RESTful API template project.](https://github.com/speakeasy-api/rest-template-go) Over the coming months, we will release RESTful API templates for the most used programming languages – [starting today with Go](https://github.com/speakeasy-api/rest-template-go). This is the template that our team forks from when we are building new APIs. The repo contains a CRUD API for a ‘user' resource which incorporates the best practices needed for a basic REST service. Our hope is that developers can use this as a foundation upon which to build their own sets of APIs. The service represents our team's opinionated stance about what makes for good RESTful API code. Specifically, the template gives you a service which is: - **Entity-based**: The resources available should represent the domain model. Each resource should have the CRUD methods implemented (even if not all are available to API consumers). In our template, we have a single resource defined (users.go). However other resources could be easily added by copying the template and changing the logic of the service layer. - **Properly Abstracted**: The transport, service, and data layers are all cleanly abstracted from one another. This makes it easy to apply updates to your API endpoints. - **Consistent**: It's important that consumers of a service have guaranteed consistency across the entire range of API endpoints and methods. In this service, responses are consistently formatted whether successfully returning a JSON object or responding with an error code. All the service's methods use shared response (http.go) and error (errors.go) handler functions to ensure consistency. - **Tested**: We believe that a blend of unit and integration testing is important for ensuring that the service maintains its contract with consumers. The service repo therefore contains a collection of unit and integration tests for the various layers of the service. - **Explorable**: It is important for developers to be able to play with an endpoint in order to understand it. We have provided Postman collections for testing out the REST endpoints exposed by the service. There is a Bootstrap Users collection that can be run using the Run collection tool in Postman that will create 100 users to test the search endpoint with. We look forward to hearing from the community what they think of the repo. We'd love to know what else people would want to see included in a template: versioning, pagination, authentication? Would people want to see more advanced features? # type-safe-vs-type-faith Source: https://speakeasy.com/blog/type-safe-vs-type-faith ### Type Faith vs Type Safe ### Introduction Type safety is a core aspect of modern programming, ensuring that errors related to mismatched or incorrect data types are caught at compile time rather than during execution. The evolution of JavaScript—a language known for its flexibility and lack of strict type enforcement—saw a significant shift with the introduction of TypeScript. TypeScript, developed by Microsoft, brought static type checking to the JavaScript ecosystem, helping developers catch errors early in the development process. However, TypeScript isn't without its limitations. While it enforces type safety within the codebase, it cannot guarantee the correctness of data coming from external sources like APIs. This becomes a critical issue when developers assert incorrect data types received from APIs, leading to potential runtime errors and system failures. The good news for API builders is that it is now possible to provide end-to-end type safety in their libraries, ensuring that data types are consistent throughout the application and API interactions. ### Type Faith Developers often opt out of implementing end-to-end type safety in their libraries due to the complexity and additional coding required. This decision places a significant burden on end users who must then ensure data type correctness in their applications, a process that is both error-prone and time-consuming. For example, consider a scenario where an API is expected to return a list of user objects, but due to changes in the backend, it starts returning mixed types (users and admin objects). A client application relying on type faith—assuming the returned data matches the expected types without validation—may perform operations that are valid for user objects but not for admin objects, leading to runtime errors and application crashes. ### Type Safe To achieve true type safety, developers can use tools like Zod to validate and enforce data types at runtime. Zod allows for the definition of schemas that describe the expected shape and type of data. Here's a simple example of how Zod can be used to enforce type safety when handling API responses ```typescript import { z } from 'zod'; // Define a schema for the user object const UserSchema = z.object({ id: z.number(), name: z.string(), email: z.string().email(), }); // Function to validate API response function validateApiResponse(data: unknown) { try { // Validate data against the schema UserSchema.parse(data); console.log('Data is valid!'); } catch (error) { console.error('Invalid data:', error); } } // Simulated API response const apiResponse = { id: 1, name: "John Doe", email: "john@example.com", }; validateApiResponse(apiResponse); ``` Using such validations, the end developer who consumes the API can trust that the data they are working with is correctly typed, reducing the risk of bugs and improving the reliability of the application. ### Conclusion Implementing end-to-end type safety requires additional effort from the developers building the API libraries. However, the benefits in terms of application stability, security, and developer productivity are immense. As the ecosystem evolves, the tools and practices around type safety continue to improve, making it increasingly feasible to achieve robust type safety across the board. By adopting these practices, developers can ensure that their applications are not only functional but also secure and resilient against type-related errors. # typescript-forward-compatibility Source: https://speakeasy.com/blog/typescript-forward-compatibility Speakeasy TypeScript SDKs now support forward compatibility features that let your SDK gracefully handle API evolution. When you add a new enum value, extend a union type, or adjust response fields, SDK users on older versions continue working without errors. This guide walks through how to enable these features in your existing SDK configuration. ## What's changing New Speakeasy TypeScript SDKs now include forward compatibility features enabled by default. If you're upgrading an existing SDK, you'll need to opt in to these features in your `gen.yaml` configuration. Here's a quick reference of the settings to add: ```yaml typescript: forwardCompatibleEnumsByDefault: true forwardCompatibleUnionsByDefault: tagged-only laxMode: lax unionStrategy: populated-fields ``` Each of these settings addresses a specific class of API evolution issues. The sections below explain what each setting does and how to configure it. 1. [Forward-compatible enums](#forward-compatible-enums) 2. [Forward-compatible unions](#forward-compatible-unions) 3. [Lax mode](#lax-mode) 4. [Smart union deserialization](#smart-union-deserialization) ### Forward-compatible enums Enums are one of the most common sources of breaking changes. When your API adds a new status, category, or type value, SDKs with strict enum validation will reject the entire response—even though the request succeeded. To enable forward-compatible enums, add this to your `gen.yaml`: ```yaml typescript: forwardCompatibleEnumsByDefault: true ``` **What this changes:** Any enum used in a response will automatically accept unknown values. Single-value enums won't be automatically opened. For finer control, use the `x-speakeasy-unknown-values: allow` or `x-speakeasy-unknown-values: disallow` extension on individual enums in your OpenAPI spec. **Example scenario:** Your notification system starts with `email` and `sms`, then you add `push` notifications. Without forward compatibility, SDK users on older versions see errors until they upgrade. With forward compatibility enabled, they receive the value gracefully: ```typescript const notification = await sdk.notifications.get(id); // Before: Error: Expected 'email' | 'sms' | 'push' // After: 'email' | 'sms' | 'push' | Unrecognized ``` When SDK users receive a known value, everything works exactly as before. Unknown values are captured in a type-safe `Unrecognized` wrapper. ### Forward-compatible unions Similar to enums, discriminated unions often get extended as your API evolves. A linked account system might start with email and Google authentication, then you add Apple or GitHub options later. To enable forward-compatible unions, add this to your `gen.yaml`: ```yaml typescript: forwardCompatibleUnionsByDefault: tagged-only ``` **What this changes:** Tagged unions will automatically accept unknown discriminator values. For finer control, use `x-speakeasy-unknown-values: allow` or `x-speakeasy-unknown-values: disallow` on specific unions in your OpenAPI spec. **Example scenario:** You add GitHub as a new linked account type. SDK users on older versions won't crash—they'll receive an `UNKNOWN` variant with access to the raw response data: ```typescript const account = await sdk.accounts.getLinkedAccount(); // Before: Error: Unable to deserialize into any union member // After: // | { type: "email"; email: string } // | { type: "google"; googleId: string } // | { type: "UNKNOWN"; rawValue: unknown } // Automatically inserted for open unions ``` ### Lax mode Sometimes the issue isn't new values being added, but missing or mistyped fields. A single missing required field can cause the entire response to fail deserialization, even when all the data SDK users actually need is present. To enable lax mode, add this to your `gen.yaml`: ```yaml typescript: laxMode: lax # or 'strict' to keep current behavior ``` **What this changes:** Lax mode applies sensible defaults when fields are missing or have minor type mismatches. It only kicks in when the payload doesn't quite match the schema—correctly documented APIs see no change in behavior. **Why this matters:** The SDK types never lie to your users. Lax mode fills in sensible defaults rather than returning incorrect data, applying only non-lossy coercions. This approach is inspired by Go's built-in JSON unmarshal behavior and Pydantic's coercion tables. For required fields that are missing, lax mode fills in zero values: | Field Type | Missing Value | Default | | ---------------- | --------------------- | ----------------------- | | Required string | `null` or `undefined` | `""` | | Required number | `null` or `undefined` | `0` | | Required boolean | `null` or `undefined` | `false` | | Required date | `null` or `undefined` | `Date(0)` (Unix epoch) | | Required literal | `null` or `undefined` | The literal/const value | | Required bigint | `null` or `undefined` | `BigInt(0)` | For nullable and optional fields, lax mode handles the common confusion between `null` and `undefined`: - Nullable field that received `undefined` is coerced to `null` - Optional field that received `null` is coerced to `undefined` Lax mode also provides fallback coercion for type mismatches: - Required string: any value coerced to string with `JSON.stringify()` - Required boolean: coerces the strings `"true"` and `"false"` - Required number: attempts to coerce strings to valid numbers - Required date: attempts to coerce strings to dates, coerces numbers (in milliseconds) to dates - Required bigint: attempts to coerce strings to bigints ### Smart union deserialization When deserializing union types, the order of options matters. The default `left-to-right` strategy tries each type in order and returns the first valid match. This works well when types have distinct required fields, but can pick the wrong option when one type is a subset of another. To enable smart union deserialization, add this to your `gen.yaml`: ```yaml typescript: unionStrategy: populated-fields ``` **What this changes:** Instead of picking the first valid match, the SDK tries all union options and returns the one with the most matching fields (including optional fields). This approach is inspired by Pydantic's union deserialization algorithm. **How it works:** The algorithm attempts to deserialize into all union options and rejects any that fail validation. It picks the candidate with the most populated fields. If there's a tie, it picks the candidate with the fewest "inexact" fields (where coercion happened, such as an open enum accepting an unknown value). **Example scenario:** You have a union of `BasicUser` and `AdminUser` where `AdminUser` has all the fields of `BasicUser` plus additional admin-specific fields. The `populated-fields` strategy correctly identifies admin users even if `BasicUser` is listed first in the union. ## Complete upgrade checklist To upgrade your existing TypeScript SDK with all forward compatibility features, add these settings to your `gen.yaml`: ```yaml typescript: forwardCompatibleEnumsByDefault: true forwardCompatibleUnionsByDefault: tagged-only laxMode: lax unionStrategy: populated-fields ``` After updating your configuration, regenerate your SDK with `speakeasy run`. The changes are backward compatible—your SDK users won't need to change their code, but they'll benefit from improved resilience to API evolution. ## What to expect These features work together to provide a robust experience for your SDK users: - **Forward-compatible enums and unions** handle additive changes to your API - **Lax mode** handles missing or mistyped fields gracefully - **Smart union deserialization** handles ambiguous type discrimination All without sacrificing the type safety and developer experience that make TypeScript SDKs valuable. If you run into any issues during the upgrade or have questions about specific scenarios, reach out to the Speakeasy team. _Note: This post focuses on TypeScript SDK behavior. Speakeasy SDKs for other languages (Python, Go, Java, and more) implement similar forward compatibility behaviors tailored to each language's idioms._ # ui-enhancements-deepobject-query-support-for-terraform Source: https://speakeasy.com/blog/ui-enhancements-deepobject-query-support-for-terraform import { Callout, ReactPlayer } from "@/lib/mdx/components"; ## 📝 Speakeasy Studio: Navigate OpenAPI Specs with Ease Speakeasy Studio now provides a structured, interactive outline for OpenAPI specs and SDK README files, significantly improving the navigation and editing experience. ## 📌 API Registry: Smarter Tracking, Seamless Organization We've redesigned the API Registry to make it even more powerful: from tracking changes at-a-glance, to sharing any API revision with a click. 🏗️ **DeepObject Query Parameter Support for Terraform Providers** Terraform provider generation now supports `style: deepObject`. This improves usability and reduces complexity for consumers by allowing them to define structured attributes in Terraform configurations, instead of needing to manually construct key-value query strings. --- ## 📝 Speakeasy Studio: Navigate OpenAPI Specs with Ease
OpenAPI specs are powerful but often overwhelming—long, complex, and difficult to navigate. Finding key details like authentication methods, endpoints, or dependencies can be time-consuming and frustrating. ### What's New? Speakeasy Studio now provides a structured, interactive outline for OpenAPI specs and SDK README files, making it effortless to: \ ✅ **Jump to key sections** like servers, security settings, and endpoints. ✅ **Quickly locate dependencies** without endless scrolling. ✅ **Improve accuracy** by reducing the risk of missing critical spec details. This update eliminates the pain of manually searching through massive API definitions. Everything you need is now neatly organized and just a click away. --- ## 📌 API Registry: Smarter Tracking, Seamless Organization
The API Registry page is the central hub for tracking OpenAPI document revisions, SDK dependencies, and API history—all in one place. ### **What's New?** We've redesigned the API Registry to make it even more powerful: \ ✅ **Improved artifact grouping** for better organization and clarity. ✅ **More structured spec representation**, making it easier to track changes at a glance. ✅ **One-click API sharing**, allowing any revision to be turned into a public URL instantly. ✅ **Namespace archiving**, helping teams manage deprecated or inactive APIs effortlessly. Whether you're maintaining API history, sharing specs, or tracking dependencies, the new API Registry page makes API management more intuitive and efficient. --- ## 🏗️ DeepObject Query Parameter Support for Terraform Providers Some APIs accept structured objects as query parameters. For example, an API might support filtering on multiple fields (`status` and `type`): ``` GET /users?filter[status]=active&filter[type]=premium ``` ### Before DeepObject Support Without `deepObject` support, consumers of a Terraform provider for this API would need to construct query strings manually. This means defining query parameters as flat key-value pairs or concatenating them into a string: ```hcl data "example_resource" "query" { query = "filter[status]=active&filter[type]=premium" } ``` This approach is unintuitive, error prone, and hard to maintain. ### With DeepObject Support Now, Speakeasy supports `style: deepObject` in OpenAPI specifications. For example: ```yaml paths: /users: get: parameters: - name: filter in: query style: deepObject explode: true schema: type: object properties: status: type: string type: type: string ``` With `deepObject` support, Terraform provider consumers can now define query parameters as structured attributes — eliminating the need to construct manual query strings: ```hcl data "example_resource" "query" { parameters = { status = "active" type = "premium" } } ``` ### Key Benefits This has a significant impact on consumer ergonomics: ✅ Eliminates manual query string construction\ ✅ Supports complex filtering and nested input\ ✅ Ensures Terraform provider behavior matches API expectations --- ## 🛠️ New Features and Bug Fixes 🐛 Based on the most recent CLI version: [**Speakeasy v2.493.8**](https://github.com/speakeasy-api/openapi-generation/releases/tag/v2.493.8) ### Platform - 🐛 Fix: Prevented compilation errors caused by special characters in `operationId`. - 🐛 Fix: Fixed handling of `null` values in maps and arrays for better representation. ### PHP - 🐝 Feat: Added support for deepObject query parameters. - 🐛 Fix: Improved query and path parameter handling. ### Go - 🐛 Fix: Resolved issues with circular references in Go usage snippets. - 🐛 Fix: Prevented Go panics related to `const` query parameters. ### TypeScript - 🐛 Fix: Reordered standalone function code for better readability. - 🐝 Feat: Introduced `APIPromise`, a custom thenable type that exposes `$inspect()` for SDK functions. ### Terraform - 🐝 Feat: Improved float/int32 number format handling. - 🐝 Feat: Enabled custom HTTP headers via provider configuration. - 🐛 Fix: Added support for nullable array types in Terraform SDKs. ### Java - 🐝 Feat: Removed Apache `httpclient` as a dependency, reducing SDK size and improving maintainability. # unity-and-c-now-available-as-generation-targets Source: https://speakeasy.com/blog/unity-and-c-now-available-as-generation-targets While we haven't had a Changelog post for a minute, the Eng team here at Speakeasy has been heads down building out a laundry list of new features and capabilities. In this Changelog we share a few of these new features including the addition of Unity and C# as generation targets. ## New Features ### C# generation C# has an active community of developers and diverse uses across desktop, web, mobile, cloud, IoT, ML, and gaming apps. We're excited to announce that Speakeasy now supports C# SDK creation! Speakeasy C# SDKs are designed to be easy to use and easy to debug. Our SDK design is guided by: - Minimal dependencies and relying on the C# standard library as much as possible. - SDK & sub SDKs implement full interfaces so that the SDKs are ready for dependency-injection. - Requests are async so that you can await them or run them asynchronously. - Nullable fields for optional objects, including fields/parameters/response and request bodies, to ensure that the user can differentiate between a field not being set and a field being set to a zero value. - Compatible with net5.0 projects for partial Unity support. ### Unity generation Unity is used by developers of all sizes, from small indie studios to large AAA studios. And now, all of these developers can take advantage of Speakeasy's newly launched support of Unity SDK creation! Our Unity SDK offers: - Native `UnityWebRequest` support so it'll work on all of Unity's target platforms - Models are Serializable and can be used with Unity Editor and plugins - Supports streaming downloads - Works with async/await and coroutines ### Multi-level Namespacing Our SDKs now support multi-level namespacing to enable interfaces that more closely match your organization's resource structure! You can read more about it [here](/docs/customize-sdks/namespaces/#multi-level-namespacing) ### Monkey Patching SDKs via .genignore We now support the use of `.genignore` to monkey patch your SDKs with any custom business logic, usage snippets, or documentation that regeneration won't override! You can read more about it in the [monkey patching documentation](/docs/customize/code/monkey-patching). ## 🐜 Improvements and Bug Fixes 🐛 ### Managed SDKs and Terraform providers - Python - id / object keywords - Ruby - Empty request body issues - publishing of typescript docs folders - validation of oneOf objects with nested references - Mandatory entity support in terraform - Support for arbitrary names in multipart file upload for Typescript ### Product onboarding - Moved API key creation for SDK generation to the backend - Long term fix for the issue where repos were getting created without the SPEAKEASY_API_KEY secret - Guarantees that each workspace will have exactly one key used for generation in GitHub and that that key is always added as a secret to every repo that gets created for that workspace - Speakeasy API Key not attached on new SDK repos For the latest on feature and bug status, check out the [Speakeasy Public Roadmap](https://roadmap.speakeasy.com/roadmap) and follow us on [X](https://twitter.com/speakeasydev?s=20) and [LinkedIn](https://www.linkedin.com/company/speakeasyapi/). Have a feature request or bug to report? Message us on [Slack](https://go.speakeasy.com/slack) or log an [issue](https://github.com/orgs/speakeasy-api/repositories) on GitHub. # Issue NODE-CLOUDFLARE-WORKERS-1 in **speakeasy** Source: https://speakeasy.com/blog/using-mcp-deep-dive import { Callout, Table } from "@/mdx/components"; import { Steps } from "nextra/components"; To abuse a [famous quote about monoids](https://stackoverflow.com/questions/3870088/a-monad-is-just-a-monoid-in-the-category-of-endofunctors-whats-the-problem), "MCP is an open protocol that standardizes how applications provide context to LLMs, what's the problem?" But even after a few hours of reading about [what MCP is](https://www.speakeasy.com/mcp/intro) and [working through an example](https://www.speakeasy.com/mcp/agents-mcp-real-world-example), it can be confusing to follow exactly what is happening when and where. What does the LLM do? What does the MCP server do? What does the MCP client do? Where does data flow, and where are choices made? This is an in-depth introduction to MCP: what it is, how it works, and a full walkthrough example showing how everything fits together and exactly what happens at each step. Specifically, we deployed a deliberately buggy Cloudflare Worker. The error surfaced in Sentry, and an AI assistant (Cline) running inside Visual Studio Code (VS Code) pulled the stack trace via the hosted **Sentry MCP Server**, opened a matching GitHub issue through a local **GitHub MCP Server**, patched the code, committed the fix, and redeployed - all under human approval. MCP cut the integration work from _M × N_ bespoke bridges to _M + N_ adapters, but it charged us in latency, security diligence, and a healthy learning curve. ## Why we need MCP When an AI assistant has to juggle real-world systems - Sentry for monitoring, GitHub for code, Jira for tickets, and Postgres for data - every extra pairing means another custom adapter, another token shim, another place to break. The result is a hairball of one-off glue code that takes maintenance time and adds security risks. MCP was created to replace that chaos with a single, predictable handshake, so any compliant host can talk to any compliant tool out of the box. ### The M × N integration tax Without MCP, each Large Language Model (LLM) host or agent (such as ChatGPT, Claude, Cline, or VS Code Copilot) plus each tool (such as Sentry, GitHub, Jira, MySQL, or Stripe) requires its own connector - that is **M hosts x N tools** bits of glue-code. Every connector re-implements: - Authentication and token refresh - Data-format mapping - Error handling and retries - Rate-limiting quirks - Security hardening The cost grows quadratically. We imagine teams will end up prioritizing a handful of integrations and calling the rest "backlog". ### One protocol to rule the connectors MCP proposes a USB-C moment for AI tooling: Every host implements MCP once, every tool exposes an MCP server once, and any host-and-tool pair can talk. Complexity collapses to **M + N**. This claim sounded too good to ignore, so we put it to the test. Before we get to our walkthrough, let's go through a quick primer: ## MCP 101 If you already speak LSP or JSON-RPC, you'll feel at home. If not, here's the 30-second cheat sheet: ### Core vocabulary
### Stateful by design MCP insists on a persistent channel, which is usually **HTTP + Server-Sent Events (SSE)** for remote servers and plain `stdio` for local processes. The server can remember per-client context (for example, auth tokens, working directory, in-progress job IDs). This is heavier than stateless REST but enables streaming diffs, long-running jobs, and server-initiated callbacks. ### Discovery flow 1. The client calls **`tools/list`** to ask the server, "What can you do?" 2. The server returns JSON describing each tool, including its name, summary, and JSON Schema for the parameters and result. 3. The host injects that JSON into the model's context. 4. When the user's prompt demands an action, the model emits a **structured call**: ```json { "name": "create_issue", "arguments": { "title": "...", "body": "..." } } ``` 5. The MCP client executes it using the transport and streams back result chunks. The conversation then resumes. ## Our demo scenario The best way to learn a new protocol is by using it to solve a real problem, so here's what we'll do: We'll create a real problem and build a real solution. ### The recurring nightmare of the 5 PM regression alert To help set the scene, imagine this scenario: It's Friday afternoon, you're the last one in the office, and just as you're packing your bag, Slack starts screaming that there is a new regression in `worker.ts:12`. We want to find the shortest route from that first Slack message to a deployed fix. ### The demo stack We want a realistic but snack-sized scenario:
```mermaid graph TB %% ─── Stack Top → Bottom ─────────────────── Worker["Cloudflare Worker (bug-demo)"] -->|"TypeError → Sentry SDK"| SentryCloud["Sentry Cloud
Project"] SentryCloud -->|"HTTPS API"| SentryMCP["Sentry MCP (SSE)"] SentryMCP -. "SSE / JSON-RPC" .-> VSCode["VS Code (Cline Host)"] VSCode -->|"stdio / JSON-RPC"| GHServer["GitHub MCP (stdio
Docker container)"] GHServer -->|"GitHub REST"| GitHubRepo["GitHub Repo"] %% Optional return arrows for clarity SentryMCP -. "list_issues" .-> VSCode GHServer -->|"create_issue / comment / PR"| VSCode ``` _A buggy Cloudflare Worker reports exceptions to Sentry, which surface through the hosted Sentry MCP (SSE) into Cline within VS Code. The same session then flows down to a local GitHub MCP (stdio) that is running in Docker, allowing the agent to file an issue, add comments, and push a pull request to the GitHub repository - all under human oversight._ ## Setup walkthrough Let's set up our stack. {

Setup requirements

} You'll need the following:
You'll also need an LLM to run the Cline agent. We used [Mistral Codestral 25.01](https://mistral.ai/) for this demo, but you can use [any LLM supported by Cline](https://docs.cline.bot/getting-started/model-selection-guide). {

Bootstrap the buggy worker

} In the terminal, run: ```bash filename="Terminal" npx -y wrangler init bug-demo ``` When prompted, select the following options: ```text filename="Terminal" ╭ Create an application with Cloudflare Step 1 of 3 │ ├ In which directory do you want to create your application? │ dir ./bug-demo │ ├ What would you like to start with? │ category Hello World example │ ├ Which template would you like to use? │ type Worker only │ ├ Which language do you want to use? │ lang TypeScript │ ├ Copying template files │ files copied to project directory │ ├ Updating name in `package.json` │ updated `package.json` │ ├ Installing dependencies │ installed via `npm install` │ ╰ Application created ╭ Configuring your application for Cloudflare Step 2 of 3 │ ├ Installing wrangler A command line tool for building Cloudflare Workers │ installed via `npm install wrangler --save-dev` │ ├ Installing @cloudflare/workers-types │ installed via npm │ ├ Adding latest types to `tsconfig.json` │ added @cloudflare/workers-types/2023-07-01 │ ├ Retrieving current workerd compatibility date │ compatibility date 2025-04-26 │ ├ Do you want to use git for version control? │ yes git │ ├ Initializing git repo │ initialized git │ ├ Committing new files │ git commit │ ╰ Application configured ╭ Deploy with Cloudflare Step 3 of 3 │ ├ Do you want to deploy your application? │ no deploy via `npm run deploy` │ ╰ Done ``` Enter your new project: ```bash filename="Terminal" cd bug-demo ``` Install the Sentry SDK npm package: ```bash filename="Terminal" npm install @sentry/cloudflare --save ``` Open your project in VS Code: ```bash filename="Terminal" code . ``` Edit `wrangler.jsonc` and add the `compatibility_flags` array with one item, `nodejs_compat`: ```json filename="wrangler.jsonc" "compatibility_flags": [ "nodejs_compat" ] ``` Visit the [Sentry setup guide for Cloudflare Workers](https://docs.sentry.io/platforms/javascript/guides/cloudflare/#setup-cloudflare-workers) and copy the example code. Paste it in `src/index.ts` and then add the intentional bug in the `fetch()` method. Edit `src/index.ts`: ```typescript filename="src/index.ts" // !mark(10:11) import * as Sentry from "@sentry/cloudflare"; export default Sentry.withSentry( (env) => ({ dsn: "https://[SENTRY_KEY]@[SENTRY_HOSTNAME].ingest.us.sentry.io/[SENTRY_PROJECT_ID]", tracesSampleRate: 1.0, }), { async fetch(request, env, ctx) { // ❌ intentional bug undefined.call(); return new Response("Hello World!"); }, } satisfies ExportedHandler, ); ``` Deploy and trigger: ```bash filename="Terminal" npm run deploy ``` In your browser, visit your Cloudflare Worker at `https://bug-demo..workers.dev`. You should see the following error: ``` Error 1101 - Worker threw exception ``` ![Cloudflare Worker error page displaying TypeError exception](/assets/mcp/intro-in-depth/cloudflare-error.png) {

Set up the Sentry MCP Server in Cline

} In VS Code, with Cline installed, follow the steps below: You may need to adjust configuration settings depending on the path of your Node and npx installation. For this guide, we used Node and npx installed with Homebrew. ![Cline VS Code extension showing the MCP servers configuration panel](/assets/mcp/intro-in-depth/cline-setup.png) 1. Click the **Cline (robot)** icon in the VS Code sidebar. 2. Click the **MCP Servers** toolbar button at the top of the Cline panel. 3. Select the **Installed** tab. 4. Click **Configure MCP Servers**. 5. Paste the Sentry MCP Server config JSON that runs `npx mcp remote https://mcp.sentry.dev/sse` in the window, then press `Cmd + s` to save. ```json filename="cline_mcp_settings.json" { "mcpServers": { "sentry": { "command": "npx", "args": ["-y", "mcp-remote", "https://mcp.sentry.dev/sse"] } } } ``` 6. Click **Done** in the top-right corner of the panel. After saving the MCP configuration, your browser should open with a prompt to authorize **Remote MCP**. ![Sentry OAuth authorization page for Remote MCP server access](/assets/mcp/intro-in-depth/sentry-auth.png) Click **Approve** so that the application can allow the Sentry MCP Server to connect with your Sentry account. {

Set up the GitHub MCP Server in Cline

} Generate a GitHub personal access token: 1. In GitHub, click your profile picture to open the right sidebar. 2. Click **Settings** in the sidebar. 3. Click **Developer settings** at the bottom of the left sidebar. 4. Expand **Personal access tokens** in the left sidebar. 5. Click **Fine-grained tokens**. 6. Press the **Generate new token** button. 7. Enter any name for your token, and select **All repositories**. 8. Select the following permissions: - Administration: Read and Write - Contents: Read and Write - Issues: Read and Write - Pull requests: Read and Write 9. Click **Generate token** and save your token for the next step. Now that we have a GitHub token, let's add the GitHub MCP Server. ![Configuring the GitHub MCP server in Cline's settings panel](/assets/mcp/intro-in-depth/github-setup.png) 1. Click the **Cline (robot)** icon in the VS Code sidebar. 2. Click the **MCP Servers** toolbar button at the top of the Cline panel. 3. Select the **Installed** tab. 4. Press **Configure MCP Servers**. 5. Paste the GitHub MCP Server config JSON that runs `docker run -it --rm -e GITHUB_PERSONAL_ACCESS_TOKEN=$GITHUB_PERSONAL_ACCESS_TOKEN ghcr.io/github/github-mcp-server` in the window, then press `Cmd + s` to save. ```json filename="cline_mcp_settings.json" { "mcpServers": { "sentry": {...}, "github": { "command": "docker", "args": [ "run", "-i", "--rm", "-e", "GITHUB_PERSONAL_ACCESS_TOKEN", "ghcr.io/github/github-mcp-server" ], "env": { "GITHUB_PERSONAL_ACCESS_TOKEN": "" } } } } ``` 6. Click **Done**. Let's take it for a spin. ## Create a GitHub repository We'll use the GitHub MCP Server to create a new repository for our demo. We asked Cline: ```text filename="Cline" Add this repository to GitHub as a private repo ``` Here's what happened next: {

System prompt with tools and task

} Cline sent a completion request to the LLM. The request contained our prompt, a list of tools, and the tool schemas. You can see how Cline built this prompt in the Cline repository at [`src/core/prompts/system-prompt`](https://github.com/cline/cline/tree/main/src/core/prompts/system-prompt). ```mermaid sequenceDiagram participant Human participant Cline participant LLM Human->>Cline: "Add this repository to GitHub as a private repo" Cline->>LLM: Sends prompt + available tools + schemas Note over Cline,LLM: Cline constructs a system prompt
containing all available tools ``` {

LLM generates a tool call

} The LLM generates a tool call based on the prompt and the tools available. The tool call is a structured XML object that contains the name of the MCP server, the name of the tool to be called, and the arguments to be passed to it. ```mermaid %%{init: { 'sequence': {'noteAlign': 'left', 'noteFontFamily': 'Monospace' } }}%% sequenceDiagram participant Human participant Cline participant LLM LLM->>Cline: Returns XML tool call Note over LLM,Cline:
github
create_repository
{
"name": "bug-demo",
"private": true
}

Cline->>Human: Asks for approval to use create_repository Human->>Cline: Approves repository creation ``` ```xml github create_repository { "name": "bug-demo", "private": true } ``` {

Cline sends the tool call to the MCP server

} Cline sends the tool call to the GitHub MCP Server using the `stdio` transport. ```mermaid %%{init: { 'sequence': {'noteAlign': 'left', 'noteFontFamily': 'Monospace' } }}%% sequenceDiagram participant Human participant Cline participant LLM participant GHMCP as GitHub MCP LLM->>Cline: Requests to create repository Cline->>Human: Asks for approval to use create_repository Human->>Cline: Approves repository creation Cline->>GHMCP: Sends JSON-RPC request via stdio Note over Cline,GHMCP: {
#160;#160;"jsonrpc": "2.0",
#160;#160;"id": "42",
#160;#160;"method": "create_repository",
#160;#160;"params": {
#160;#160;#160;#160;"name": "bug-demo",
#160;#160;#160;#160;"private": true
#160;#160;}
} ``` ```json filename="stdio" { "jsonrpc": "2.0", "id": "42", "method": "create_repository", "params": { "name": "bug-demo", "private": true } } ``` - The message is sent over the already-open `stdio` pipe as a single UTF-8 line, typically terminated by `\n`, so the server can parse it line by line. - The `id` is an opaque request identifier chosen by Cline; the server will echo it in its response to let the client match replies to calls. - All MCP tool invocations follow the same structure - only the method and parameters change. {

GitHub MCP Server processes the request

} The GitHub MCP Server receives the tool call and processes it. It calls the GitHub API to create a new repository with the specified name and privacy settings, then parses the response from the API. ```mermaid sequenceDiagram participant GHMCP as GitHub MCP participant GHAPI as GitHub API GHMCP->>GHAPI: Makes API call to create repository Note over GHMCP,GHAPI: GitHub MCP Server translates
JSON-RPC to GitHub REST API call ``` {

GitHub MCP Server sends the response back to Cline

} The GitHub MCP Server sends the response back to Cline over the `stdio` transport. ```mermaid %%{init: { 'sequence': {'noteAlign': 'left', 'noteFontFamily': 'Monospace' } }}%% sequenceDiagram participant Human participant Cline participant LLM participant GHMCP as GitHub MCP participant GHAPI as GitHub API GHAPI->>GHMCP: Returns API response GHMCP->>Cline: Sends JSON-RPC response via stdio Note over GHMCP,Cline: {
#160;#160;"jsonrpc": "2.0",
#160;#160;"id": "42",
#160;#160;"result": {
#160;#160;#160;#160;"id": 123456789,
#160;#160;#160;#160;"name": "bug-demo",
#160;#160;#160;#160;...
#160;#160;}
} Cline->>Human: Shows result for verification ``` ```json filename="stdio" { "jsonrpc": "2.0", "id": "42", "result": { "id": 123456789, "name": "bug-demo", "visibility": "private", "default_branch": "main", "git_url": "git://github.com/speakeasy/bug-demo.git", "etc": "..." } } ``` - The response contains the `id` of the request and the result of the tool call. - The result is a JSON object that contains the details of the newly created repository. - Cline receives the response and displays it in the UI. - The response is also passed to the LLM for further processing and is now available in the context for the next prompt. {

Cline displays the result

} Cline displays the result of the tool call in the UI, and the LLM can use it in subsequent prompts. ```mermaid sequenceDiagram participant Human participant Cline participant LLM Cline->>Human: Displays repository creation result Cline->>LLM: Passes repository info into context Note over Cline,Human: Cline UI shows successful
repository creation details ``` ```json { "id": 123456789, "name": "bug-demo", "visibility": "private", "default_branch": "main", "git_url": "git://github.com/speakeasy/bug-demo.git", "etc": "..." } ``` {

Cline pushes the repository to GitHub

} Cline pushes the new repository to GitHub using the `git` command. ```mermaid %%{init: { 'sequence': {'noteAlign': 'left', 'noteFontFamily': 'Monospace' } }}%% sequenceDiagram participant Human participant Cline participant LLM participant GHMCP as GitHub MCP participant GHAPI as GitHub API LLM->>Cline: Request git command execution Cline->>Human: Asks for approval to run git command Human->>Cline: Approves command execution Cline->>GHAPI: Executes git command to push repository Note over Cline,GHAPI: git remote add origin git@github.com:speakeasy/bug-demo.git
git push -u origin main Human->>Cline: Confirms successful push ``` ```bash filename="Terminal" git remote add origin git@github.com:speakeasy/bug-demo.git && \ git push -u origin main ```
## Fixing the bug using MCP That was the fiddly part, which only happens once. Now we can fix the bug. {

Giving Cline a task

} In Cline, we asked: ```text filename="Cline" 1. Fetch the latest issue from Sentry. 2. Create a new GitHub issue with a link back to the Sentry issue and a description. 3. Fix the bug based on the issue from Sentry. 4. Commit your changes in a new branch with an appropriate message and reference to both Sentry and the GitHub issues. 5. Push new branch to GitHub. 6. Open a PR with reference to the GitHub issue. ``` {

Cline sends the request to the LLM

} Cline sends the request to the LLM, which generates a tool call to the Sentry MCP Server to fetch the latest issue. ```mermaid %%{init: { 'sequence': {'noteAlign': 'left', 'noteFontFamily': 'Monospace' } }}%% sequenceDiagram participant Human participant Cline participant LLM participant SentryMCP as Sentry MCP Human->>Cline: Requests to fix bug using MCP Cline->>LLM: Sends multi-step task request LLM->>Cline: Returns tool call for list_issues Cline->>Human: Asks for approval to run tool Human->>Cline: Approves tool call Cline->>SentryMCP: Sends JSON-RPC request via SSE Note over Cline,SentryMCP: {
#160;#160;"jsonrpc": "2.0",
#160;#160;"id": "42",
#160;#160;"method": "list_issues",
#160;#160;"params": {
#160;#160;#160;#160;"sortBy": "last_seen"
#160;#160;}
} ``` The JSON-RPC request sent to the Sentry MCP Server looks like this: ```json filename="stdio" { "jsonrpc": "2.0", "id": "42", "method": "list_issues", "params": { "sortBy": "last_seen" } } ``` {

Sentry MCP Server processes the request

} The Sentry MCP Server processes the request and calls the Sentry API to fetch the latest issue. It returns the result to Cline. ```mermaid sequenceDiagram participant Cline participant LLM participant SentryMCP as Sentry MCP participant SentryAPI as Sentry API SentryMCP->>SentryAPI: Queries for latest issues SentryAPI->>SentryMCP: Returns issue list SentryMCP->>Cline: Sends formatted issue data via SSE Note over SentryMCP,Cline: Response contains formatted
issue summary with error details ``` ```json filename="stdio" { "jsonrpc": "2.0", "id": "42", "result": { "content": [ { "type": "text", "text": "# Issues in **speakeasy**\n\n## NODE-CLOUDFLARE-WORKERS-1\n\n**Description**: TypeError: Cannot read properties of undefined (reading 'call')\n**Culprit**: Object.fetch(index)\n**First Seen**: 2025-04-21T14:21:10.000Z\n**Last Seen**: 2025-04-21T15:05:33.000Z\n**URL**: https://speakeasy.sentry.io/issues/NODE-CLOUDFLARE-WORKERS-1\n\n# Using this information\n\n- You can reference the Issue ID in commit messages (e.g. `Fixes `) to automatically close the issue when the commit is merged.\n- You can get more details about a specific issue by using the tool: `get_issue_details(organizationSlug=\"speakeasy\", issueId=)`" } ] } } ``` {

Cline analyzes the Sentry issue

} After receiving the issue from Sentry, Cline requests additional details to understand the stack trace: ```mermaid %%{init: { 'sequence': {'noteAlign': 'left', 'noteFontFamily': 'Monospace' } }}%% sequenceDiagram participant Human participant Cline participant LLM participant SentryMCP as Sentry MCP participant SentryAPI as Sentry API Cline->>LLM: Provides initial issue data LLM->>Cline: Requests more detailed information Cline->>Human: Asks for approval to run get_issue_details Human->>Cline: Approves tool call Cline->>SentryMCP: Calls get_issue_details Note over Cline,SentryMCP: {
#160;#160;"jsonrpc": "2.0",
#160;#160;"id": "43",
#160;#160;"method": "get_issue_details",
#160;#160;"params": {
#160;#160;#160;#160;"organizationSlug": "speakeasy",
#160;#160;#160;#160;"issueId": "NODE-CLOUDFLARE-WORKERS-1"
#160;#160;}
} SentryMCP->>SentryAPI: Retrieves detailed issue information SentryAPI->>SentryMCP: Returns stack trace and details SentryMCP->>Cline: Sends comprehensive error data Cline->>LLM: Provides complete error context Note over LLM,Cline: LLM analyzes stack trace to
identify exact error location ``` ```json filename="stdio" { "jsonrpc": "2.0", "id": "43", "method": "get_issue_details", "params": { "organizationSlug": "speakeasy", "issueId": "NODE-CLOUDFLARE-WORKERS-1" } } ``` The Sentry MCP Server returns error details: ```text filename="response" **Description**: TypeError: Cannot read properties of undefined (reading 'call') **Culprit**: Object.fetch(index) **First Seen**: 2025-04-21T14:21:10.756Z **Last Seen**: 2025-04-21T15:05:33.000Z **URL**: https://speakeasy.sentry.io/issues/NODE-CLOUDFLARE-WORKERS-1 ## Event Specifics **Occurred At**: 2025-04-21T15:05:33.000Z **Error:** ``` TypeError: Cannot read properties of undefined (reading 'call') ``` **Stacktrace:** ``` index.js:6633:25 (Object.withScope) index.js:6634:14 index.js:3129:12 index.js:3371:21 index.js:3140:14 index.js:2250:26 (handleCallbackErrors) index.js:3141:15 (handleCallbackErrors.status.status) index.js:6996:33 index.js:7021:79 index.js:7090:16 (Object.fetch) ``` Using this information: - You can reference the IssueID in commit messages (for example, `Fixes NODE-CLOUDFLARE-WORKERS-1`) to automatically close the issue when the commit is merged. - The stack trace includes both first-party application code as well as third-party code, so it's important to triage to first-party code. ``` The stack trace reveals the exact nature and location of the bug: It's due to the `(undefined).call()` in `src/index.ts` on line 12. ![Sentry error details showing the stack trace and TypeError](/assets/mcp/intro-in-depth/sentry-error-details.png) This response is passed to the LLM, which uses it to generate the next tool call. {

Cline creates a GitHub issue

} Next, at the request of the LLM, Cline uses the GitHub MCP Server to create an issue documenting the bug: ```mermaid %%{init: { 'sequence': {'noteAlign': 'left', 'noteFontFamily': 'Monospace' } }}%% sequenceDiagram participant Human participant Cline participant LLM participant GHMCP as GitHub MCP participant GHAPI as GitHub API LLM->>Cline: Decides to create GitHub issue Cline->>Human: Asks for approval to use create_issue Human->>Cline: Approves issue creation Cline->>GHMCP: Sends create_issue request Note over Cline,GHMCP: {
#160;#160;"jsonrpc": "2.0",
#160;#160;"id": "44",
#160;#160;"method": "create_issue",
#160;#160;"params": {
#160;#160;#160;#160;"owner": "speakeasy",
#160;#160;#160;#160;"repo": "bug-demo",
#160;#160;#160;#160;"title": "Fix bug in index.ts",
#160;#160;#160;#160;"body": "This issue addresses the bug described in the Sentry issue: https://speakeasy.sentry.io/issues/NODE-CLOUDFLARE-WORKERS-1"
#160;#160;}
} GHMCP->>GHAPI: Creates issue via GitHub API GHAPI->>GHMCP: Returns issue creation confirmation GHMCP->>Cline: Sends issue creation result Cline->>LLM: Provides issue details for next steps Cline->>Human: Shows GitHub issue creation success ``` ```json filename="stdio" { "jsonrpc": "2.0", "id": "44", "method": "create_issue", "params": { "owner": "speakeasy", "repo": "bug-demo", "title": "Fix bug in index.ts", "body": "This issue addresses the bug described in the Sentry issue: https://speakeasy.sentry.io/issues/NODE-CLOUDFLARE-WORKERS-1" } } ``` The GitHub MCP Server confirms that the issue has been created: ```json filename="stdio" { "jsonrpc": "2.0", "id": "44", "result": { "id": 1, "number": 1, "state": "open", "locked": false, "title": "Fix bug in index.ts", "body": "This issue addresses the bug described in the Sentry issue: https://speakeasy.sentry.io/issues/NODE-CLOUDFLARE-WORKERS-1", "etc": "..." } } ``` ![GitHub issue created about the Sentry error](/assets/mcp/intro-in-depth/github-issue.png) {

Cline examines the codebase

} To fix the bug, the LLM needs to have the code in context. The LLM initiates a tool call to read the source code of the Cloudflare Worker. ```mermaid %%{init: { 'sequence': {'noteAlign': 'left', 'noteFontFamily': 'Monospace' } }}%% sequenceDiagram participant Human participant Cline participant LLM participant VSCodeFS as VS Code File System LLM->>Cline: Requests source code Cline->>Human: Asks for approval to read file Human->>Cline: Approves file read Cline->>VSCodeFS: Uses read_file tool Note over Cline,VSCodeFS:
src/index.ts
VSCodeFS->>Cline: Returns file contents Cline->>LLM: Provides source code Note over LLM,Cline: LLM examines code and identifies
the bug: (undefined).call() ``` Since we're working directly in the VS Code editor, Cline uses the `read_file` tool: ```xml filename="Tool Call" src/index.ts ``` Cline sends the result of the tool call to the LLM, which now has the full context of the codebase. After examining the code, the LLM responds with a proposed fix. {

The LLM generates the fix and Cline applies it

} ```mermaid %%{init: { 'sequence': {'noteAlign': 'left', 'noteFontFamily': 'Monospace' } }}%% sequenceDiagram participant Human participant Cline participant LLM participant VSCodeFS as VS Code File System LLM->>Cline: Generates fix to remove buggy code Note over LLM,Cline: #60;replace_in_file#62;
#60;path#62;src/index.ts#60;/path#62;
#60;diff#62;
#60;#60;#60;#60;#60;#60;#60; SEARCH
#160;#160;#160;#160;(undefined).call()#59;
=======
>>>>>> REPLACE
#60;/diff#62;
#60;/replace_in_file#62; Cline->>Human: Shows proposed file change for approval Human->>Cline: Approves code change Cline->>VSCodeFS: Applies the fix by removing the buggy line VSCodeFS->>Cline: Confirms file update Cline->>LLM: Reports successful code change Note over Cline,LLM: LLM can now proceed with
creating branch and committing fix ``` The LLM generates a fix for the bug, which is then sent to Cline: ```xml src/index.ts (undefined).call(); ``` {

Cline creates a new branch and commits the fix

} ```mermaid %%{init: { 'sequence': {'noteAlign': 'left', 'noteFontFamily': 'Monospace' } }}%% sequenceDiagram participant Human participant Cline participant LLM participant GHAPI as GitHub API LLM->>Cline: Requests to create a new branch Cline->>Human: Asks for approval to run git command Note over Cline,Human:
git checkout -b fix-bug-NODE-CLOUDFLARE-WORKERS-1
true
Human->>Cline: Approves command execution Cline->>Cline: Creates new branch locally LLM->>Cline: Requests to commit the fix Cline->>Human: Asks for approval to commit changes Note over Cline,Human: Commit message references both Sentry
and GitHub issue IDs for auto-linking Human->>Cline: Approves commit LLM->>Cline: Requests to push branch to GitHub Cline->>Human: Asks for approval to push Human->>Cline: Approves push Cline->>GHAPI: Pushes branch to GitHub repository ``` Cline creates a new branch for the fix: ```xml git checkout -b fix-bug-NODE-CLOUDFLARE-WORKERS-1 true ``` This opens the VS Code terminal and creates a branch: ```bash filename="Terminal" git checkout -b fix-bug-NODE-CLOUDFLARE-WORKERS-1 ``` Cline then commits the fix: ```xml git add src/index.ts && git commit -m "Fixes NODE-CLOUDFLARE-WORKERS-1 and closes #1" true ``` Then, Cline pushes the new branch to GitHub: ```xml git push origin fix-bug-NODE-CLOUDFLARE-WORKERS-1 true ``` {

Cline opens a PR

} ```mermaid %%{init: { 'sequence': {'noteAlign': 'left', 'noteFontFamily': 'Monospace' } }}%% sequenceDiagram participant Human participant Cline participant LLM participant GHMCP as GitHub MCP participant GHAPI as GitHub API LLM->>Cline: Requests to create a pull request Cline->>Human: Asks for approval to use create_pull_request Human->>Cline: Approves PR creation Cline->>GHMCP: Sends create_pull_request request Note over Cline,GHMCP: {
#160;#160;"jsonrpc": "2.0",
#160;#160;"id": "47",
#160;#160;"method": "create_pull_request",
#160;#160;"params": {
#160;#160;#160;#160;"owner": "speakeasy",
#160;#160;#160;#160;"repo": "bug-demo",
#160;#160;#160;#160;"title": "Fix bug in index.ts",
#160;#160;#160;#160;"head": "fix-bug-NODE-CLOUDFLARE-WORKERS-1",
#160;#160;#160;#160;"base": "main",
#160;#160;#160;#160;"body": "This PR fixes the bug described in the Sentry issue..."
#160;#160;}
} GHMCP->>GHAPI: Creates PR via GitHub API GHAPI->>GHMCP: Returns PR creation confirmation GHMCP->>Cline: Sends PR creation result Cline->>LLM: Provides PR details Cline->>Human: Shows GitHub PR creation success ``` Finally, Cline creates a pull request (PR) with the fix: ```json filename="stdio" { "jsonrpc": "2.0", "id": "47", "method": "create_pull_request", "params": { "owner": "speakeasy", "repo": "bug-demo", "title": "Fix bug in index.ts", "head": "fix-bug-NODE-CLOUDFLARE-WORKERS-1", "base": "main", "body": "This PR fixes the bug described in the Sentry issue: https://speakeasy.sentry.io/issues/NODE-CLOUDFLARE-WORKERS-1 and closes #1" } } ``` The GitHub MCP Server confirms the PR creation. ![GitHub PR showing the bug fix about Sentry issue](/assets/mcp/intro-in-depth/github-pr.png) {

Human approval and deployment

} The final step requires human approval. The developer reviews the PR, approves the changes, merges the PR, and deploys: ```bash filename="Terminal" npm run deploy ``` A quick visit to the Cloudflare Worker URL confirms the fix is working, and Sentry shows no new errors. ![Fixed Cloudflare Worker successfully serving Hello World response](/assets/mcp/intro-in-depth/fixed-worker.png)
## What we learned We learned a lot about MCP, both the good and the bad. Here are our key takeaways: ### The good: Protocol unification The MCP approach delivered on its promise. We only had to set up each service once, rather than having to build custom bridges between every pair of services. This was our first concrete experience with the theoretical _M + N_ vs _M × N_ difference. Consider our modest demo setup with just three components (the Cline host, Sentry server, and GitHub server):
The difference is small, with just a few components, but scales dramatically. With 10 hosts and 10 tool backends (100 potential connections), MCP requires just 20 adapters. Moreover, we found that the JSON Schema system improved tooling discoverability. When Cline connected to a server, it automatically received comprehensive documentation about available operations without having to consult external API references. ### The challenges: Latency, security, learning curve MCP may not be suitable for all use cases. We encountered several challenges that could limit its applicability. #### Latency The MCP approach introduces additional layers between the LLM and the APIs. This comes with a latency cost. In our testing, each MCP hop added a little time overhead, which is negligible for most use cases but could become significant in latency-sensitive applications or during complex multi-step workflows. This isn't a flaw in the protocol, but rather a trade-off for the convenience of having a single, unified interface. The latency is manageable for most applications, but it's worth considering if you're building a performance-critical application. #### Security Authentication represents one of the more challenging aspects of MCP. The protocol requires the secure handling of access tokens, which introduces additional security considerations. 1. **Token management**: Each server requires its own authentication approach (OAuth for Sentry, API tokens for GitHub). 2. **Permission scoping**: The user needs to grant permissions for each server, which can be cumbersome. 3. **Token refresh**: OAuth flows with refresh tokens add complexity. This may be a symptom of the relative immaturity of the ecosystem, but most MCP clients do not yet support OAuth flows or token refreshes. This is exactly why the Sentry MCP Server is called via `npx mcp-remote`, which is a wrapper MCP server that handles the OAuth flow and token refresh for you: ```json { "command": "npx", "args": [ "-y", "mcp-remote", // a wrapper MCP server "https://mcp.sentry.dev/sse" ] } ``` #### Learning curve While the protocol is reasonably straightforward for developers familiar with JSON-RPC, we encountered a few hurdles. 1. **Sparse documentation:** The specification is comprehensive, but practical guides are limited. 2. **Debugging challenges:** When tools failed, error messages weren't always clear. The first time we tried to run the Sentry MCP Server, we encountered an authentication error that was difficult to diagnose. ### Ecosystem maturity MCP is still in its early days. The available servers are primarily reference implementations rather than production-ready services. We identified several areas needing improvement. 1. **Standardization**: Common operations (like CRUD) aren't yet standardized across servers. 2. **Host support**: LLM host support is limited and extremely variable. Some hosts support only `tools`, while others support `resources` and `tools`. Some hosts support only `stdio`, while others support `SSE`. Despite these challenges, the direction is promising. Each new MCP server adds disproportionate value to the ecosystem by enabling any compliant host to connect to it. ## Future improvements Here's a wishlist of improvements we think would benefit the MCP ecosystem: ### Performance optimizations Future MCP implementations could benefit from several performance optimizations: 1. **Connection optimization**: Using streaming HTTP instead of SSE for long-lived connections would reduce the need for persistent connections and improve performance. (This is already in development in the MCP SDK and spec.) 2. **Schema caching**: Hosts and LLMs could cache tool schemas to avoid repeated discovery calls and wasted token usage. 3. **Request batching**: Grouping related operations into a single request would increase efficiency by reducing round trips between the client and server. 4. **Partial schema loading**: Loading only the schemas for tools likely to be used in a given context could reduce token usage and improve tool selection. Our Cline session for this problem sent 413,300 tokens and received 2,100 in total. This is a lot of tokens for a single session, and we could have saved plenty of tokens by caching the schemas and using partial schema loading. ### Enhanced security The security model could be strengthened with: 1. **Granular permissions**: Token scopes limited to specific tools rather than entire servers would allow for granular permissions. This is part of the MCP Specification (in the form of MCP roots) but isn't widely supported by clients or servers yet. 2. **Approval workflows**: Using more sophisticated approval UIs for dangerous operations could ensure the user is aware of the implications of each action and help them avoid prompt injection attacks. 3. **Audit logging**: Comprehensive tracking of all MCP operations would improve the security model. ## Verdict and recommendations We're excited about the potential of MCP and think the protocol will become a key part of the AI ecosystem. However, we'd recommend caution for production use. Audit the security of any MCP server you use and be prepared to work around the ecosystem's current fragmentation. While everyone is still figuring out the security details, perhaps deactivate YOLO mode and stick to human-in-the-loop workflows for now. # Run only the test at line 42 Source: https://speakeasy.com/blog/vitest-vs-jest import { Callout, Table } from "@/mdx/components"; This article was updated in January 2025 to reflect the release of [Vitest 3](https://github.com/vitest-dev/vitest/releases/tag/v3.0.0), which introduces new features and improvements, as well as some breaking changes. Be sure to check the [official Vitest documentation](https://vitest.dev/blog/vitest-3.html) and migration guide for further details. Effective testing frameworks are essential in building reliable JavaScript applications, helping you minimize bugs and catch them early to keep your customers happy. Choosing the right testing framework saves hours of configuration hassles and improves developer experience. This post compares [Jest](https://jestjs.io/) and [Vitest](https://vitest.dev/), popular JavaScript testing frameworks that support unit testing, integration testing, and [snapshot testing](https://jestjs.io/docs/snapshot-testing). **Jest** — created by the Facebook team, is the most widely used JavaScript testing framework. **Vitest** — a fast-growing new testing framework. Originally built for [Vite](https://vite.dev/), the popular development server and JavaScript bundler, Vitest can now be used with any JavaScript project, not just those using Vite. ## Which JavaScript testing framework is right for you? We think Vitest is best, unless you're using a framework or library that has better Jest support, such as React Native. The table below compares some features of the two testing frameworks. **If you're starting a new project, use Vitest**. With ES module, TypeScript, JSX, and PostCSS support out of the box, Vitest is the right choice for almost every modern JavaScript app.
\* Experimental
\+ Using Babel
\*\* Includes inline workspace support At Speakeasy, we've tried both Jest and Vitest, and ultimately chose Vitest – even without Vite in our setup. We decided to document our experience and dive deeper into both Vitest and Jest, comparing the two testing frameworks in terms of their features, performance, and developer experience to further help you decide which is best for your use case. We'll also share our experience of migrating from Jest to Vitest for our TypeScript SDK and how we're using Vitest for our new automated API testing feature, which is currently in beta, for Speakeasy-created SDKs. ## Jest's beginnings: From Facebook to open source Back in 2011, no JavaScript testing framework existed that met the Facebook (now Meta) team's testing needs, so they built Jest. Jest was open-sourced in 2014, shortly after React was open-sourced, and as React rapidly gained in popularity, so did Jest. Popular React project starter [Create React App](https://create-react-app.dev/) integrated Jest as its default testing framework in 2016 and Jest soon became the go-to testing tool for React developers. When Next.js, the most popular React framework, included built-in Jest configuration in version 12, Jest's dominance in the React ecosystem was secured. Ownership of Jest was transferred to the [OpenJS Foundation](https://engineering.fb.com/2022/05/11/open-source/jest-openjs-foundation/) in 2022 and the framework is currently maintained by a core group of contributors external to Meta. ## Jest features Jest's popularity is in part thanks to the extensive Jest API that handles a variety of testing needs, including snapshots, mocking, and code coverage, making it suitable for most unit testing situations. Jest works with Node.js and frontend frameworks like React, Angular, and Vue, and can be used with TypeScript using Babel or the `ts-jest` library to transpile TypeScript to JavaScript. Consider the following example snapshot test: ```javascript filename="link.test.js.snap" test("Checkout link renders correctly", () => { const tree = renderer .create(Go to checkout) .toJSON(); expect(tree).toMatchSnapshot(); }); ``` The [`test`](https://jestjs.io/docs/api#testname-fn-timeout) method runs a test. The Jest [`expect` API](https://jestjs.io/docs/expect) provides modifier and matcher (assertion) methods that simplify checking whether a value is what you expect it to be. Among the matcher methods available are: - `toBe()` - `toHaveProperty()` - `toMatchSnapshot()` Jest's extensive set of modifiers and matchers makes error messages more detailed, helping you pinpoint why a test fails. The [jest-extended](https://github.com/jest-community/jest-extended) library, maintained by the Jest community, provides additional matchers to expand testing functionality. You can use Jest's [mock functions](https://jestjs.io/docs/mock-functions) to mock objects, function calls, and even npm modules for faster testing without relying on external systems. This is especially useful for avoiding expensive function calls like calling a credit card charging API in the checkout process of an app. To run code before and after tests run, Jest provides handy [setup and teardown functions](https://jestjs.io/docs/setup-teardown): `beforeEach`, `afterEach`, `beforeAll`, and `afterAll`. ```javascript filename="cityDb.test.js" beforeAll(() => { return initializeCityDatabase(); }); afterAll(() => { return clearCityDatabase(); }); ``` ```javascript filename="cityDb.test.js" beforeEach(() => { initializeCityDatabase(); }); afterEach(() => { clearCityDatabase(); }); test("city database has Vienna", () => { expect(isCity("Vienna")).toBeTruthy(); }); test("city database has San Juan", () => { expect(isCity("San Juan")).toBeTruthy(); }); ``` When running tests, you can add the `--watch` flag to only run tests related to changed files. ```shell jest --watch ``` While Vitest doesn't have Jest's [Interactive Snapshot Mode](https://jestjs.io/docs/snapshot-testing#interactive-snapshot-mode), which enables you to update failed snapshots interactively in watch mode, this feature has been [added to Vitest's TODO list](https://github.com/vitest-dev/vitest/issues/2229). Jest also has a multi-project runner for running tests across multiple projects, each with their own setup and configuration, using a single instance of Jest. ## Getting started with Jest To set up a basic test, install Jest as a development dependency: ```shell npm install --save-dev jest ``` Create a test file with `.test` in its name, for example, `sum.test.js`: ```javascript filename="sum.test.js" const sum = require("./sum"); test("adds 1 + 2 to equal 3", () => { expect(sum(1, 2)).toBe(3); }); ``` This code imports a `sum` function that adds two numbers and then checks that `1` and `2` have been correctly added. Add the following script to your `package.json` file: ```json filename="package.json" { "scripts": { "test": "jest" } } ``` Run the test using the following command: ```shell npm run test ``` Jest finds the test files and runs them. Jest will print the following message in your terminal if the test passes: ```shell PASS ./sum.test.js ✓ adds 1 + 2 to equal 3 (5ms) ``` ## The origin of Vitest: The need for a Vite-native test runner The introduction of in-browser ES modules support led to the creation of the popular JavaScript bundler and development server Vite, which uses ES modules to simplify and speed up bundling during development. While it's possible to use Jest with Vite, this approach creates separate pipelines for testing and development. Among other factors, Jest requires separate configuration to transpile ES modules to CommonJS using Babel. A test runner that supports ES modules would integrate better with Vite and simplify testing. Enter Vitest. Developed by core Vite team member, Anthony Fu, Vitest is built on top of Vite, but you can use it in projects that don't use Vite. Vite's rise in popularity can be attributed to its simplicity and performance. Unlike traditional JavaScript bundlers that can be slow when running development servers for large projects, the Vite development server loads fast, as no bundling is required. Instead, Vite transforms and serves source code as ES modules to the browser on demand, functionality that allows for fast Hot Module Replacement (HMR) - the updating of modules while an app is running without a full reload - even for large applications. In development, the main advantages of using Vitest with Vite are their performance during development, ease of integration, and shared configuration. Vitest's growing adoption is partly driven by the widespread success of Vite. ## Vitest features Vitest has built-in ES module, TypeScript, JSX, and PostCSS support and many of the same features as Jest, like the `expect` API, snapshots, and code coverage. Vitest's Jest-compatible API makes migrating from Jest easy. Take a look at the differences between the frameworks in the [migrating from Jest guide](https://vitest.dev/guide/migration.html#migrating-from-jest) in the Vitest docs. With Vitest 3, several features have been enhanced. The code coverage functionality now automatically excludes test files by default, providing cleaner coverage reports. The mocking capabilities have also been expanded, with the new version including spy reuse for already mocked methods, reducing test boilerplate, and adding powerful new matchers like `toHaveBeenCalledExactlyOnceWith`: ```javascript test('vitest mocking example', () => { const mock = vi.fn(); mock('hello'); // New matcher for more precise assertions expect(mock).toHaveBeenCalledExactlyOnceWith('hello'); // Spy reuse example const obj = { method: () => {} }; const spy = vi.spyOn(obj, 'method'); // Spy is automatically reused if spyOn is called again const sameSpy = vi.spyOn(obj, 'method'); expect(spy === sameSpy).toBe(true); }); ``` In contrast to Jest, Vitest uses watch mode by default for a better developer experience. Vitest searches the ES module graph and only reruns affected tests when you modify your source code, similar to how Vite's HMR works in the browser. This makes test reruns fast - so fast that Vitest adds a delay before displaying test results in the terminal so that developers can see that the tests were rerun. Vitest supports [Happy DOM](https://github.com/capricorn86/happy-dom) and [jsdom](https://github.com/jsdom/jsdom) for DOM mocking. While Happy DOM is more performant than jsdom, which is used by Jest, jsdom is a more mature package with a more extensive API that closely emulates the browser environment. When running tests, Vitest runs a Vite dev server that provides a UI for managing your tests. Vitest 3 significantly improves the UI experience with several new features: - Run individual tests or test suites right from the UI for easier debugging. - Quickly spot and fix issues with automatic scrolling to failed tests. - Get a clearer view of your test dependencies with toggleable `node_modules` visibility. - Better organize your tests with improved filtering, search, and management controls. To view the UI, install `@vitest/ui` and pass the `--ui` flag to the Vitest run command: ```shell vitest --ui ``` ![Vitest UI](/assets/blog/vitest-vs-jest/vitest-ui.png) Take a look at the StackBlitz [Vitest Basic Example](https://stackblitz.com/fork/github/vitest-dev/vitest/tree/main/examples/basic?initialPath=__vitest__/) for a live demo of the Vitest UI. At Speakeasy, we've found Vitest's UI to be a valuable tool for debugging. Vitest also has Rust-like [in-source testing](https://vitest.dev/guide/in-source) that lets you run tests in your source code: ```javascript filename="add.ts" export function add(...args: number[]) { return args.reduce((a, b) => a + b, 0) } // in-source test suites if (import.meta.vitest) { const { it, expect } = import.meta.vitest it('add', () => { expect(add()).toBe(0) expect(add(1)).toBe(1) expect(add(1, 2, 3)).toBe(6) }) } ``` While using separate test files is recommended for more complex tests, in-source testing is suitable for unit testing small functions and prototyping, making it especially useful for writing tests for JavaScript libraries. Vitest's recent experimental [browser mode](https://vitest.dev/guide/browser/) feature allows you to run tests in the browser, instead of simulating them in Node.js. [WebdriverIO](https://webdriver.io/) is used for running tests, but you can use other providers. You can also browse in headless mode using WebdriverIO or [Playwright](https://playwright.dev/). Vitest developed browser mode to improve test accuracy by using a real browser environment and streamline test workflows, but you may find browser mode slower than using a simulated environment as it needs to launch a browser, handle page rendering, and interact with browser APIs. Vitest 3 introduces powerful new CLI features that upgrade test execution flexibility. You can now exclude specific projects using patterns with `--project=!pattern`, making it easier to manage test runs in monorepos or large projects. The new line-number-based test filtering allows you to run specific tests by their location in the file: ```shell vitest path/to/test.ts:42 # Exclude all projects matching the pattern vitest --project=!packages/internal-* ``` Other experimental features include [type testing](https://vitest.dev/guide/features.html#type-testing) and [benchmarking](https://vitest.dev/guide/features.html#benchmarking). ## Getting started with Vitest The set up and execution of a basic test in Vitest is similar to Jest. First, install Vitest as a development dependency: ```shell npm install --save-dev vitest ``` Create a test file with `.test` or `.spec` in its name, for example, `sum.test.js`: ```javascript filename="sum.test.js" import { expect, test } from "vitest"; import { sum } from "./sum.js"; test("adds 1 + 2 to equal 3", () => { expect(sum(1, 2)).toBe(3); }); ``` This code imports a `sum` function that adds two numbers and then checks that `1` and `2` have been correctly added. Compared to Jest, this basic test is a little different. Vitest uses ES module imports, and for the sake of explicitness, it does not provide global APIs like `expect` and `test` by default. You can configure Vitest to provide [global APIs](https://vitest.dev/config/#globals) if you prefer or are migrating from Jest. Add the following script to your `package.json` file: ```json filename="package.json" { "scripts": { "test": "vitest" } } ``` Run the test using the following command: ```shell npm run test ``` Vitest finds the files and runs them, and will print the following message in your terminal if the test passes: ```shell ✓ sum.test.js (1) ✓ adds 1 + 2 to equal 3 Test Files 1 passed (1) Tests 1 passed (1) Start at 02:15:44 Duration 311ms ``` Note that Vitest starts in watch mode by default in a development environment. ## Choosing between Vitest and Jest Let's compare the two testing frameworks in terms of performance, developer experience, community and ecosystem, and their usage with Vite. ### Performance Performance comparisons of Vitest and Jest have delivered conflicting results, as the outcome depends on what you're testing and how you configure the testing tool. One blog post reports that [Jest is faster than Vitest](https://bradgarropy.com/blog/jest-over-vitest) when running all the tests for the author's blog website. [Another comparison](https://dev.to/mbarzeev/from-jest-to-vitest-migration-and-benchmark-23pl), which performed 1,256 unit tests on a production web app that uses Vite and Vue, found that Vitest was faster than Jest in most tests. [Yet another comparison](https://uglow.medium.com/vitest-is-not-ready-to-replace-jest-and-may-never-be-5ae264e0e24a) found Jest to be almost twice as fast as Vitest for testing a production app. Even if your benchmarking finds Jest is faster than Vitest, there are ways to improve Vitest's performance, as explained in the [improving performance guide](https://vitest.dev/guide/improving-performance.html#improving-performance) in the Vitest docs. For example, you can disable test [isolation](https://vitest.dev/config/#isolate) for projects that don't rely on side effects and properly clean up their state, which is often the case for projects in a `node` environment. Vitest 3 introduces significant performance enhancements with several key changes. The test runner now supports better concurrent execution with suite-level test shuffling for improved parallelism. Snapshot handling has been optimized to reset state properly during retries and repeats, reducing test flakiness. The new version also improves memory usage through smarter spy handling, automatically reusing mocks for methods that have already been spied on instead of creating new ones. In watch mode, Vitest often outperforms Jest when rerunning tests, as it only reruns tests affected by code changes. While Jest also only reruns changed tests in watch mode, it relies on checking uncommitted Git files, which can be less precise, as not all detected changes may be relevant to the tests you're running. ### Developer experience Both Jest and Vitest have comprehensive, well-organized, and easy-to-search documentation. The Jest docs include guides for specific types of tests such as [timer mocks](https://jestjs.io/docs/timer-mocks) and using Jest with other libraries, databases, web frameworks, and React Native. Vitest's [getting started guide](https://vitest.dev/guide/) sets it apart. It includes StackBlitz examples that you can use to try Vitest in the browser without setting up a code example yourself. The getting started guide also has example GitHub repos and StackBlitz playgrounds for using Vitest with different web frameworks. Vitest's ES module support gives it a significant advantage over Jest, which only offers [experimental support for ES modules](https://jestjs.io/docs/ecmascript-modules). Jest uses [Babel](https://babeljs.io/) to transpile JavaScript ES Modules to CommonJS, using the [`@babel/plugin-transform-modules-commonjs`](https://babeljs.io/docs/babel-plugin-transform-modules-commonjs) plugin. By default, Babel excludes Node.js modules from transformation, which may be an issue if you use an [ES-module-only library](https://gist.github.com/sindresorhus/a39789f98801d908bbc7ff3ecc99d99c) like [`react-markdown`](https://remarkjs.github.io/react-markdown/), in which case you'll get this commonly seen error message: ```javascript SyntaxError: Unexpected token 'export' ``` To fix this issue, use the [`transformIgnorePatterns`](https://jestjs.io/docs/configuration#transformignorepatterns-arraystring) option in your `jest.config.js` file to specify that the Node module is an ES module package that needs to be transpiled by Babel: ```javascript filename="jest.config.js" /** @type {import('jest').Config} */ const config = { ... "transformIgnorePatterns": [ "node_modules/(?!(react-markdown)/)" ], }; module.exports = config; ``` Jest supports TypeScript using Babel, which requires some [configuration](https://jestjs.io/docs/getting-started#using-typescript). Overall, Jest requires some configuration to work with ES modules and TypeScript, which work out of the box with Vitest. Less configuration means happier developers. [RedwoodJS](https://redwoodjs.com/), the full-stack, open-source web framework, [is in the process of migrating from Jest to Vitest](https://www.youtube.com/watch?v=zVY4Nv104Vk&t=271). The RedwoodJS team found Vitest much better suited to working with ES modules compared to Jest. ### Community and ecosystem While [npm stats](https://tanstack.com/stats/npm?packageGroups=%5B%7B%22packages%22%3A%5B%7B%22name%22%3A%22vitest%22%7D%5D%7D%2C%7B%22packages%22%3A%5B%7B%22name%22%3A%22jest%22%7D%5D%7D%5D&range=365-days&transform=none&binType=weekly&showDataMode=complete&height=400) shows that Jest is downloaded more frequently than Vitest, Vitest usage is rapidly increasing, outpacing Jest in growth. ![npm trends graph: Jest vs. Vitest - number of npm downloads over time](/assets/blog/vitest-vs-jest/npm-trends-jest-vs-vitest.png)
According to the [State of JS 2023 survey](https://2023.stateofjs.com/en-US/libraries/), Vitest has seen a rapid rise in popularity and positive opinions between 2021 and 2023. By contrast, Jest experienced a rapid rise in popularity and positive opinions between 2016 and 2020, but this momentum slowed between 2021 and 2023, with opinions becoming more mixed. This shift may be due to developers adopting Vitest, which solves one of the main pain points of Jest: ES Module support. Vite and Vitest claimed many of the top spots in the [survey](https://2023.stateofjs.com/en-US/awards/), while Jest made an appearance as the second most most-loved library, after Vite. Of the four most popular JavaScript frontend frameworks and libraries, two use Jest, and two use Vitest. React uses Jest and Angular added experimental Jest support in 2023 with the release of Angular version 16 to modernize its unit testing. Jest's popularity and the fact that it's been around for longer means there are more blog posts and [Stack Overflow questions about Jest](https://stackoverflow.com/questions/tagged/jestjs) than [Vitest](https://stackoverflow.com/questions/tagged/vitest), which is useful for figuring out uncommon testing situations and debugging. Many large companies use Jest, but Vitest is [gaining traction in prominent projects](https://vitest.dev/guide/#projects-using-vitest), including frameworks like Vue and Svelte. Many of the projects using Vitest are built with Vue. This is no surprise, as Evan You, the creator of Vue, also created Vite, and one of Vite's core team members developed Vitest. For React developers, Jest's compatibility with mobile development using React Native and [Expo](https://expo.dev/) is an important advantage. React Native and Expo testing documentation recommend using Jest, and Expo provides the [`jest-expo`](https://github.com/expo/expo/tree/main/packages/jest-expo) library to simplify testing for Expo and React Native apps. Vitest, meanwhile, offers support for React Native using the [`vitest-react-native`](https://github.com/sheremet-va/vitest-react-native) library, which is developed by a Vitest team member and is a work in progress. Vitest is under more rapid development than Jest, as can be seen by the number of recent commits to the [Vitest GitHub repo](https://github.com/vitest-dev/vitest/commits/main/) compared to the [Jest GitHub repo](https://github.com/jestjs/jest/commits/main/). Recently, Evan You founded [VoidZero](https://voidzero.dev/), a company building an open-source, unified development toolchain by bringing together some of the most popular JavaScript toolchain libraries: Vite, Vitest, Rolldown, and Oxc. With $4.6 million in seed funding, VoidZero is likely to accelerate Vitest's development and boost its popularity. ### Using with Vite If Vite is part of your development toolchain, Vitest's easy integration with Vite is a compelling advantage, delivering a streamlined setup through shared configuration. Jest is not fully supported by Vite, but you can get it to work using the vite-jest library, with some [limitations](https://github.com/haoqunjiang/vite-jest/tree/main/packages/vite-jest#limitations-and-differences-with-commonjs-tests). Using Jest with Vite adds complexity as you need to manage testing and building your app as separate processes. However, Angular uses Vite and Jest. The Angular team considered using Vitest but chose Jest because of its widespread adoption among Angular developers and its maturity. ## Migrating from Jest to Vitest: Insights from our experience at Speakeasy Our experience migrating from Jest to Vitest at Speakeasy revealed clear advantages, particularly in speed and ease of setup. In January, when we rebuilt our TypeScript SDK, switching to Vitest gave us a noticeable runtime boost with zero configuration. Unlike Jest, which required Babel integration, TSJest setup, and multiple dependencies, Vitest worked out of the box, allowing us to drop five dependencies from our project. Vitest's compatibility with Jest's API made the migration smooth, and the intuitive UI provided a browser view with real-time test updates – a significant productivity boost. Since July, we've expanded Vitest's role in our process by introducing a test generation product, where clients can create Vitest tests for their API SDKs, maintaining minimal dependencies with only Zod and Vitest required. Vitest has quickly become a strong choice for future-proofing, advancing rapidly with the support of VoidZero and its integration into a modern toolchain that aligns with frameworks like Vue. ## Adding automated API testing for Speakeasy-created SDKs using Vitest When we rebuilt our TypeScript SDK generation – which doesn't use Vite – we switched from Jest to Vitest and saw a significant improvement in performance. Vitest needed zero configuration – it just worked. Vitest's Jest-compatible API made the migration straightforward, and we've found that Vitest has most of Jest's features as well as some extras, like its feature-packed, intuitive UI for viewing and interacting with tests. We are now in the process of expanding our use of Vitest to our customers' Speakeasy-generated SDKs with an automated API testing feature that generates API tests that will help you make more robust and performant APIs, faster and cheaper. Our own internal SDKs have Vitest tests, which we used to add this feature. Test generation is currently available in Typescript, Python, and Go. Customers can enable test generation in a generated Speakeasy SDK using their OpenAPI doc or using a `tests.yaml` file. We have plans to further improve testing including: - Providing a mock server to run the generated tests against. - Adding integration with CI/CD using our GitHub Action. - Adding support for more languages. - Adding end-to-end testing using the [Arazzo specification](/openapi/arazzo), which allows for more complex testing scenarios. - Integration with the Speakeasy web app. ## Conclusion: Which testing framework is better? Vitest's many experimental features simplify testing and the framework is under active development, a fact that may skew this comparison even more in its favor in the coming years. While both libraries are easy to get started with, Vitest is easier to set up for modern projects using ES modules and TypeScript, and it's easier to use with modern npm libraries that are ES modules only. We think that Vitest is a good successor to Jest. Picking a technology means betting on the future, and we're betting on Vitest. # webhook-security Source: https://speakeasy.com/blog/webhook-security Interesting fact: [80% of API producers sign their webhook requests with HMAC-SHA256](https://ngrok.com/blog-post/get-webhooks-secure-it-depends-a-field-guide-to-webhook-security). Request signing provides **authenticity** by enabling you, the webhook consumer, to verify who sent the webhook. However, webhooks aren't so different from any other API request. They're just an HTTP request from one server to another. So why not use an API key just like any other API request to check who sent the request? Signing requests does give extra security points, but why do we collectively place higher security requirements on webhook requests than API requests? ## What is request signing? Disclaimer: this is not an exhaustive explanation of cryptographic signatures. This is a practical introduction to what is meant by “request signing” in this article and by the average webhooks service. ```tsx function sign_request(request) { // The secret is never included in the request // Also, the request contents form the signature input request.headers["x-signature"] = sign(secret, request.body); } ``` When consuming the request, you'd do something like: ```tsx function verify_request(request) { actual = request.headers["x-signature"]; // This is the same secret used for the sender and consumer expected = sign(secret, request.body); return actual == expected; } ``` ## The three security benefits of signing requests There are three benefits to signing your requests (in addition to encrypting the request in transit over TLS): 1. **Reduced risk of leaking secrets**: Though traditional API requests are likely over HTTPS, the application server is likely not the TLS-terminating gateway. Once decrypted, it's common for API Keys to leak into logs, queues, third parties and traverse several layers of infrastructure. Signed requests never contain the sensitive secret, so there's a smaller surface area that the secret will touch and thus reduced risk of leaking the secret. 2. **Replay protection**: With an API Key, you have weaker guarantees on when the message was sent. Since you can include a timestamp and/or a nonce in the signed message, you have stronger guarantees somebody didn't attempt to maliciously recycle the same request. 3. **Integrity**: With an API Key, you have fewer guarantees that the request contents were created by the same party that “owns” the API Key. Maybe some malicious HTTP client middleware added or modified it? With signing, the signature is built from the request contents. ## Why aren't most API requests signed? If there are so many benefits, why aren't most API requests signed? While it's a lot less common, some big names like [Amazon](https://docs.aws.amazon.com/IAM/latest/UserGuide/reference_sigv.html), [Azure](https://learn.microsoft.com/en-us/azure/azure-app-configuration/rest-api-authentication-hmac) and [Oracle](https://docs.oracle.com/en/cloud/saas/marketing/crowdtwist-develop/Developers/HMACAuthentication.html) all employ request signing in their APIs. Indeed, there is even a [standard for signing HTTP requests](https://datatracker.ietf.org/doc/rfc9421/). So are API keys just a phase? I'll posit that the primary reason most people don't opt for signing their API requests is because of herd mentality. That's a valid reason. If simple API Keys are [good enough for Stripe requests](https://docs.stripe.com/keys) then they're probably good enough for you, too. Is request signing really that much more secure? Security is like stacked slices of [Swiss cheese](https://en.wikipedia.org/wiki/Swiss_cheese_model) with lots of randomly placed holes. The more layers you have, the more holes the attacker has to find. Knowing when to stop adding layers is hard. There's always better security. Inline with mantras like "don't roll your own crypto” and “not invented here” it's safer and easier to follow the wisdom of the crowd. Having said that, there are some principled reasons for why you wouldn't bother signing your API requests: - **API Keys are good enough**: At least for the comparison in this article, you can assume the API producer does enforce HTTPS for both API requests and webhook endpoints. If the request will be encrypted over TLS, the risk of 2 & 3 above are massively diminished. TLS guarantees confidentiality and integrity. This makes the integrity guarantees provided by 3 somewhat redundant as the attacker would need to be able to view or modify the unencrypted request, which would likely require compromising the request sender's machine or infrastructure. At which point, as the victim, requests being replayed is the least of your concerns. - **Complexity of implementation**: For example, how you will sign server sent events or streams? Which parts of the HTTP request will be included in the signature input? Webhook requests are mostly small-ish POST requests which makes them practical to sign. - **Performance overhead**: signed requests are guaranteed to be slower than traditional API key requests, especially for big payloads. ## Why do we place higher security requirements on webhooks? It is valid to argue that webhook requests don't inherently deserve stronger security measures than traditional API requests. The security benefits of request signing can provide value in either context. Therefore, there is a double standard. If you do decide to use shared secrets for webhooks, it is a reasonable decision and that doesn't mean your webhooks service is insecure. Having said that, there are some reasons why webhooks often receive this different security treatment: - **Webhooks are untrusted URLs**: It's one thing if you accidentally hand your API key to a malicious site—now they can impersonate you. It's another if Stripe, for example, sends a secret to a malicious site—now that site can impersonate Stripe. What is the risk of a misconfigured webhook URL vs a misconfigured API request? Then again, if a URL is truly untrusted, Stripe shouldn't be sending data there in the first place. In practice, Stripe and similar providers trusts URLs their customers configure. By using a separate secret for each customer, a single compromised secret doesn't compromise all customers. - **Historical precedent**: This precedent likely dates back to the [PubSubHubbub standard from 2008](https://github.com/pubsubhubbub/PubSubHubbub/commit/3aec180d9170afdd816085e6d3d3301fd188c514#diff-99eb0f15a3e4d003ab1cbe7378d330f366134af4d473e79876812fee073d3d0bR140) and snowballed from there. Signing is also included as a best practice in the more recent [Standard Webhooks](https://www.standardwebhooks.com/). As before, most webhooks services follow the wisdom of the crowd. - **Non-HTTPS Webhooks**: While increasingly rare, some webhooks still use plain HTTP. Request signing can provide some level of protection against potential man-in-the-middle attacks when TLS is absent. Most modern webhook providers, however, enforce HTTPS by default and still use signing. Regardless, request signing adds valuable security layers to any type of request, which is why [we support API Producers to configure request signing in our generated SDKs at Speakeasy](https://www.speakeasy.com/docs/customize/webhooks). We're following the wisdom of the crowd, too. # why-an-api-platform-is-important Source: https://speakeasy.com/blog/why-an-api-platform-is-important This is the first in a series of conversations that we'll be having with leading API development engineers and managers about how they're approaching API development within their organizations. If you or someone you would like to sit down and talk to us, [please get in touch](https://rfnxkmu4ddz.typeform.com/to/b2bfTMUG). Tl;dr - API platforms are important for turning API development into a repeatable task for internal developers. Repeatability is critical for exposing a consistent experience to external developers. - An API platform should handle all the API ops (every aspect of API creation that's not the business logic). Ideally, you should be able to put server code in, and get a production-ready API out. - It's important to formalize an API strategy early, ideally long before you've launched anything publicly. A little upfront planning can avoid a pile of tech debt later. - For most companies who sell to developers, APIs are at the heart of DevEx. API platforms can often become the foundation for the larger DevEx team. - Webhooks are great and should be considered any time there's a "push" use case. ## Introduction _Roopak Venkatakrishnan is a director of engineering at Bolt. He is responsible for managing the platform & infrastructure org, as well as the merchant org working to democratize commerce by providing one-click checkout to sellers. Prior to Bolt, Roopak was at Spoke (acq. by Okta) and held senior positions at Google and Twitter._ ## What is an API Platform **_Nolan: You've been working on building out Bolt's API platform. Could you explain what the purpose of an API platform is? If it is successfully built, how does it impact the day to day work of the developers in the company?_** Roopak: Yeah, simply put, you want to give your internal developers a great experience when building APIs. And then when you ship an API as a product, you want to make sure the API is holistically thought through; that certain standards are followed throughout. So for example, when an external developer already uses two of your APIs, and then they go on to use a third API that you add later: the errors look the same, the endpoints, the style, the response objects, everything is the same. Generally, once they start working with your product, there should be no surprises. It should look similar and holistic. So that's one part where an API platform can start helping. It can define a nice pattern for the teams, their product, and make sure APIs are treated holistically across the company. But the second, more interesting part of it, especially for internal teams, is there's going to be a lot of shared components. Things that you don't want to just be using. As an example, you don't want everyone to be building their own rate limiting. It could also be the way you do your docs, it could be guides, it could be, authentication. Every team shouldn't be trying to build their own, it should just be handled for you. **So what I would say is that an API platform should take away all the API ops, basically everything that isn't business logic.** Let product developers handle the business logic and then the rest of it is handled by the API platform. An assembly line for APIs, raw business logic goes in, and a productized API comes out. **_Nolan: Bolt is an API as a product company, but for lots of companies APIs are a secondary interface. How do you think this changes the need for an API platform?_** Roopak: Interestingly, Bolt was not, until recently, an API-driven company. In the sense that, when I joined, one of the things that bothered me about our APIs was that it was just different all over the place. The way we handled authentication, the way we did rate limits, the way our docs were published, our errors, even our observability, across different APIs, it was inconsistent. To go back to the question, whenever you start providing an API externally, you should have an API platform, and in fact, I think essentially every company does have an API platform. They just don't realize it's an actual thing that they maintain or provide. Usually, it's just two developers, who are kind of managing this in the style guides or runbooks they provide to the rest of the team. It's very similar to say, maintaining Dev & Prod in a company's early days, right? Like, you don't have a developer productivity engineer, when you are eight engineers, but someone still does it 30% of the time, you're just not accounting for it. And I think you should just be thoughtful and realize that at some point, you do need to start accounting for it and be like, "hey, if we don't do this, we're just going to end up in a bad place." Ultimately, someone needs to be thinking about all these API concerns. Because, here's the worst part of it, this is not something you can go back and fix, right? **Here's the problem with an API, you've released it, and then you're stuck with it. If you want to make a change, you have to beg every single external developer to make the change.** Let's just say you add a field to an API without realizing it. Even if it's not documented, you cannot remove it anymore. You know why? Because the minute it's out there, somebody's started using it. So, it's actually one of those things you should think about earlier, more than anything else, because it's no take backs. ## How to design API architecture **_Nolan: Bolt's public APIs are RESTful, but you also provide webhooks. What're your thoughts on when to use different frameworks?_** Roopak: I'm biased. I really, really like web hooks. I would say, in some sense, it's easier to offer web hooks to start off with, than to offer APIs. For example, I have built a lot of stuff off of some companies' web hooks before I started using their APIs. But I do think it's important that you offer both. I think that web hooks are the push and APIs are the pull. Web hooks should be the way you notify customers about changes in your system. And I believe that almost any system which has an API is going to have changes in their system that they want to notify their customers about. **_Nolan: If someone was designing their API architecture today, what advice would you give them?_** Roopak: I have an anecdote. Spoke, the previous company I worked at was very interesting. I was an early employee, so we were building from the ground up. From day one, we said, "Hey, we're going to build our entire app on a public API". We're just not going to publish the documentation. So that way, whenever we want to become an API driven system, we are already there. I thought it was a genius idea! But it was really tough. At some point, we finally realized that it was slowing us down. Because to make changes to a public API, you have to be really thoughtful, and you know, an internal endpoint not so much. So, we tried doing it all public, but it didn't end up working out. And when we published our actual public API, it ended up that we didn't just take everything from what we already had, they were almost there, but still required changes. So, we published a style guide for public APIs. We said, "Okay, this is how our external APIs are gonna look." But it was an interesting learning for me, I like that we did it. I just think that, in the end, the style guide approach kind of worked for us, at least for a while, we were small enough that we were not adding too many endpoints. Eventually though, it just becomes harder. Because the more engineers you have, there's just someone who's just not going to follow the guide. Someone is going to make a mistake, and then you need to have a group of people who review the API changes, and then it just starts getting more and more expensive. And that's why API platforms are ultimately necessary. ## The API Platform team at Bolt **_Nolan: When did you create the API platform team at Bolt, and how did you know it was the right time to start it?_** Roopak: Our APIs had existed for the people who needed them, but we weren't necessarily trying to be API-first. But at some point, we realized, you know, the kinds of customers we want, they actually need a good API. We can't publish something shitty, because that's just not going to work. So that's sort of when we realized we needed a formalized API platform. I think the answer to the right time is, it's like, if you're a company, which is offering an API as a product, you should think about this on day one. But if you're starting to offer an API as a secondary interface, and even if you don't have a dedicated team around it, you should have a group of people who sit and then think through this and then say, "Hey, how are we going to make sure what we expose is what we want to expose." You better think through this, you can't do it after the fact. It has to be before. Otherwise, you're going to pay back a lot of tech debt. **_Nolan: How would you define the mandate of the API platform team at Bolt_** Roopak: It's actually slowly growing, right? Initially, it was, help our engineering teams do the basics of shipping an API. The team started with building our API tooling: detecting backward incompatible changes, helping with our Doc's, and so on and so forth. That is growing, it's going to be every part of the API lifecycle: key rotation, authentication – rate limiting is its own beast. Every part of API tooling is going to be something that this team does. But the team is actually formally called DevEx, which is developer experience, because that's the ultimate goal.This team is going to to be the team that interfaces with developers every which way. One day, it may be a fully-fledged developer portal, but today it's mainly focused on tooling to produce consistently great APIs for our customers. **_Nolan: What is something your team recently accomplished that you would like to shine a light on? Anything you're particularly proud of?_** Roopak: We recently shifted API Doc generation to be much more in line with our code. We're creating the API reference directly from our server code, and having it live right beside the code. I'm very excited about that, mainly because I believe that API docs are super important; it's the end-product people see. You can have a beautiful API, but if you have bad API docs, you're going to be set a couple of steps back. So we're using Open API 3.0. And we've constructed the whole thing where all the models are separate. Even when you're starting to build an API, you can be like, "Oh, what are the models that we use in our different APIs", and you can actually look them up. Here's the request models that we use, all the various different things. So you can look at them per request, and then you can look at what we use overall. And it's all right beside the code, which I'm personally a fan of because it encourages developers to think about what the customer will see if they are making a change to the code base. You can't just write the API code in isolation and throw it over the fence to someone else to write the API reference and documentation. The end-product is just not going to work well that way. **_Nolan: What are currently the API platform team's biggest goals?_** Roopak: Oh there's a lot of different things. I'm not going to jump into everything. We're redoing some of our authentication, our key rotation, all of that. The other big goal is improving how quickly we get our partner developers being able to use our APIs. That's one of the metrics that we're starting to track. How soon can a developer get a key and make their first successful API call? And there are so many more tiny things to improve the internal experience for developers that we're trying to get done. **_Nolan: If you ask developers building public APIs at Bolt what their biggest challenge is, what would they say?_** Roopak: Well, we do ask our developers this, and until recently, it was things like: I don't know where the docs are, how to edit them. It was very complicated. So we picked that up. I think, probably right now, it would be something like, I don't know what our definition for a good API is. And you know, that's something that we need to come up with, and evangelize. Like, we're redoing errors right now as an example. We just haven't published a comprehensive guide yet. And right now, it's much more informal, like you get guidance from this team. So I would say that's probably the next thing. We know how we want endpoints to look. We want to make it clear to our developers what's a good API, so new APIs are in-line with the rest of Bolt's existing APIs. **_Nolan: Do internal APIs factor into an API platform team's remit?_** Roopak: Right now it's handled differently. We're still trying to figure out how we want to do this. We want our API platform to focus on the external stuff first. That's more important to get in shape. And then internal stuff. It's interesting, as long as the APIs are performing, even if the API change is backwards incompatible, it's not the end of the world. You can create an entirely new endpoint, and then make every service transition over because you can ping the team and be like, "hey, switch over," and then you can get rid of the old endpoint. Sure, it might take a week, and it's a little bit of a pain, but we're shipping a lot of new external APIs. So we want to make sure we got that in shape. So internal, not yet. I do think there is an ideal world where I would like an API platform to manage both internal and external. Build a framework, maybe internally use something like gRPC. And, you know, the platform would help other teams generate internal docs for their endpoints, so that any team can easily provide a good DevEx internally. But, we're not big enough for that yet. As you become a much bigger company, you do want internal teams to be treated similarly to your external customers. But, you know, it all depends on the size of the company. **_Nolan: A closing question we like to ask everyone: any new technologies or tools that you're particularly excited by? Doesn't have to be API related._** Roopak: Ah, this is a very hard question, because for me, this changes every two days. I try out loads of new tools and then some of them just stick. So I can tell you all the random things that I have been trying out and using recently. I cannot pick a favorite because it's too hard! I got an invite to [Arc](https://thebrowser.company/), the new browser, so that is something which I've been playing with. I have switched my terminal to use [Warp](https://www.warp.dev/), and I use [Graphite](https://graphite.dev/) for code review. I think Graphite is just friggin great. This is on the tooling side. On the development side, it's been things like [Temporal](https://temporal.io/) and [Tailscale](https://tailscale.com/). And finally, I mean, this is not a new tool, but I'm moving all my personal projects that I build onto [Cloudflare](https://www.cloudflare.com/), trying to make everything run on workers and pages and whatever else they offer. # Why API producers should care about JSONL Source: https://speakeasy.com/blog/why-api-producers-should-care-about-jsonl When end users expect near-instant responses (as they do), API producers reach for streaming responses. ![An image macro shows Pablo Escobar (from the series, Narcos) waiting in three different locations. The meme text reads, "ME EXACTLY 100MS AFTER CLICKING SEND IN YOUR APP," implying the meme represents users' frustration with app waiting periods.](/assets/blog/why-api-producers-should-care-about-jsonl/jsonl/pablo-waiting.jpg) Yet, many APIs still force users to wait for complete responses before displaying anything, creating that dreaded spinner experience that kills engagement and conversion rates. We believe that streaming responses will become the norm for most APIs, especially those that wrap or extend LLMs. In this post, we'll directly address the common misconception that streaming is a complex and time-consuming feature to implement. If you're struggling to implement streaming, the problem is likely that you're ignoring the simplest tool in your arsenal: JSONL. We'll compare traditional request-response implementations with streaming alternatives, demonstrating not just the user experience benefits of streaming but also the surprisingly minimal code changes it requires. If your team has been putting off streaming support because it seems like a major project, we hope this guide will show you a faster path forward. ## What is JSONL? JSON Lines (JSONL) is a text format that represents a sequence of JSON objects, with one object per line. It looks like this: ```jsonl {"name": "Labrador", "size": "large"} {"name": "Wire Fox Terrier", "size": "medium"} {"name": "Schnauzer", "size": "medium"} ``` ## OK, but _why_ is JSONL even a thing? As we'll discuss in more detail below, JSONL's many benefits boil down to one main point: JSONL data can be processed or emitted record by record while it is read from a file or received over a network without needing to load the entire dataset into memory first. This makes JSONL inherently streamable. Let's break that down a bit: ### JSONL can be parsed line by line There is no need to parse the entire dataset before you begin processing JSONL. This means you can start sending data to your users as soon as you have it, rather than waiting for the entire response to be ready. And users can start consuming data the moment they receive it, rather than waiting for the entire response to be sent. Each chunk you send is a complete response, after all. ### JSONL uses less memory on the server and client Because only one line (representing a single JSON object or value) needs to be in memory for parsing at any given time, JSONL has a low memory footprint compared to the footprint for parsing a large standard JSON array. This makes it feasible to work with terabyte-scale datasets on machines with limited memory. ### We don't need to know the size of the dataset beforehand Adding more records to a JSONL file is straightforward and efficient. You can append new JSON entries as new lines to the end of the file without needing to parse or modify the existing data. This is ideal for logging systems or any application where data is continuously generated. ### JSONL can be parallelized Because each line in a JSONL file is independent, processing is easily parallelized. This means you can take advantage of multicore processors to process multiple lines simultaneously, significantly speeding up data processing tasks. ### JSONL plays well with Unix tools The line-based nature of JSONL makes it compatible with a wide range of standard Unix command-line utilities like `grep`, `sed`, `awk`, `head`, and `tail`. This makes it easy to explore and filter data without needing to load it into a specialized tool or library. For example, you can use `grep` to quickly find all records that match a certain pattern or `head` to view the first few lines of a large dataset. ### JSONL is human-readable Subjectively, JSONL is easier to read than a large JSON array. Each line represents a single record, so you can scan through the data without being overwhelmed by the sheer volume of information. This is especially helpful when debugging or exploring data. Copy a line, and you know you have a complete record - there's no risk of missing a closing bracket or comma. ### Error handling is easier When processing a JSONL file line by line, an error encountered while parsing one line (for example, a malformed JSON object) does not necessarily invalidate the remaining lines in the file. Parsers can be designed to skip or log problematic lines and continue processing the rest of the data. ### It takes your dog for a walk Just checking whether you're still with us. But seriously, it does make your code cleaner, which can lead to more relaxing time off. Who knows what else it might do for you? ## Why is JSONL important _now_? ![An image macro shows a screenshot from Toy Story. Buzz Lightyear gestures widely, with his other arm around a despondent Woody, and the text overlay reads "AI" at the top and "AI EVERYWHERE" at the bottom, suggesting the ubiquity of AI technology.](/assets/blog/why-api-producers-should-care-about-jsonl/jsonl/buzz-lightyear-ai.jpg) Large Language Models (LLMs) generate text in chunks. This means that as LLMs produce output, they can stream text to users in real time, rather than waiting for the entire response to be generated. Users expect this kind of responsiveness, and APIs that don't provide it risk losing users to competitors that do. Streaming allows AI agents to respond to or act on data as it arrives, rather than requiring them to wait for long and expensive text completions to finish. This makes AI agents far more efficient and agile. ## This seems too fast and loose, where's the spec? JSONL is a simple format, and as such, it doesn't have a formal specification. However, the [JSON Lines website](https://jsonlines.org/) provides a good overview of the format. Here's the TL;DR: - JSONL is always UTF-8 encoded. - Each line is a valid JSON value. - Lines are separated by a newline (`\n`) character. While not prescriptive, the JSON Lines website also adds that the recommended file extension is `.jsonl` and that the MIME type may be `application/jsonl` (though this is not formalized yet, and `application/x-ndjson` is also used). So, it seems more like a convention than a specification. But the fact that it is so simple and widely used means that you can rely on it being supported by most programming languages and libraries. ## Streaming vs traditional request-response We'll ask an OpenAI model to generate some text, then do the same thing with streaming, and see what is sent over the wire. ### Requirements This example assumes you have a working OpenAI API key set as an environment variable called `OPENAI_API_KEY`. If you don't have one, you can sign up for a free [OpenAI account](https://platform.openai.com/signup), [get a new API key](https://platform.openai.com/api-keys), then run the following command to set your API key: ```bash # Set your OpenAI API key here export OPENAI_API_KEY=sk-... ``` ### Traditional request-response In a traditional request-response model, you would send a single request to the OpenAI API and wait for the entire response to be generated before processing it. Let's see how this looks in practice. Run the following command to send a request to the OpenAI API and receive a response: ```bash curl https://api.openai.com/v1/responses \ -H "Content-Type: application/json" \ -H "Authorization: Bearer $OPENAI_API_KEY" \ -H "Accept: application/json" \ --silent \ --no-buffer \ -d '{ "model": "gpt-4.1", "input": "Write a short sentence about tamborines." }' ``` The response will look something like this: ```json { "id": "resp_1234567890abcdef", "object": "response", "output": [ { "id": "msg_682ddf545708819897cf1cc0c76269780de9aa243ddb5cc3", "type": "message", "status": "completed", "content": [ { "type": "output_text", "annotations": [], "text": "Tambourines are handheld percussion instruments that create a jingling sound when shaken or struck." } ], "role": "assistant" } ] # ... snip ... } ``` We're most interested in the `content` field, which contains the actual text generated by the model. See how the entire response is wrapped in a JSON object? This means that the entire response must be generated before it can be sent to the client. For this tiny example, that isn't a big deal. But imagine if the model were generating a full-length novel or a long technical document. There would be a significant delay before the user saw any output, and they would have to wait for the entire response to be generated before they could start reading it. ### Streaming Now let's try the same thing with streaming. When we use this command, the OpenAI API will send the response in chunks as it is generated, allowing us to start processing it immediately: ```bash curl https://api.openai.com/v1/responses \ -H "Content-Type: application/json" \ -H "Authorization: Bearer $OPENAI_API_KEY" \ -H "Accept:text/event-stream" \ --no-buffer \ --silent \ -d '{ "model": "gpt-4.1", "input": "Write a short sentence about tamborines.", "stream": true }' ``` OpenAI, like many modern APIs, uses Server-Sent Events (SSE) to stream responses. In this format, each piece of data is typically a JSON object, prefixed by `data:`. The response will look something like this: ```text event: response.output_text.delta data: {"type":"response.output_text.delta","item_id":"msg_682de0b36c888198a8dfc9890806fbeb0443a479c4e91c02","output_index":0,"content_index":0,"delta":"A"} event: response.output_text.delta data: {"type":"response.output_text.delta","item_id":"msg_682de0b36c888198a8dfc9890806fbeb0443a479c4e91c02","output_index":0,"content_index":0,"delta":" tamb"} event: response.output_text.delta data: {"type":"response.output_text.delta","item_id":"msg_682de0b36c888198a8dfc9890806fbeb0443a479c4e91c02","output_index":0,"content_index":0,"delta":"our"} event: response.output_text.delta data: {"type":"response.output_text.delta","item_id":"msg_682de0b36c888198a8dfc9890806fbeb0443a479c4e91c02","output_index":0,"content_index":0,"delta":"ine"} event: response.output_text.delta data: {"type":"response.output_text.delta","item_id":"msg_682de0b36c888198a8dfc9890806fbeb0443a479c4e91c02","output_index":0,"content_index":0,"delta":" is"} event: response.output_text.delta data: {"type":"response.output_text.delta","item_id":"msg_682de0b36c888198a8dfc9890806fbeb0443a479c4e91c02","output_index":0,"content_index":0,"delta":" a"} ... we cut a whole bunch of these out for brevity ... event: response.output_text.delta data: {"type":"response.output_text.delta","item_id":"msg_682de0b36c888198a8dfc9890806fbeb0443a479c4e91c02","output_index":0,"content_index":0,"delta":" music"} event: response.output_text.delta data: {"type":"response.output_text.delta","item_id":"msg_682de0b36c888198a8dfc9890806fbeb0443a479c4e91c02","output_index":0,"content_index":0,"delta":"."} event: response.output_text.done data: {"type":"response.output_text.done","item_id":"msg_682de0b36c888198a8dfc9890806fbeb0443a479c4e91c02","output_index":0,"content_index":0,"text":"A tambourine is a small percussion instrument with jingling metal disks, often shaken or struck to add rhythm and sparkle to music."} ``` ### This isn't simpler at all You might look at that SSE output with its `event:` and `data:` lines and think it doesn't immediately scream "simplicity". And you're right, SSE has its own protocol for framing events. It's powerful for sending named events, and many APIs like OpenAI use it effectively. Each `data:` payload in that SSE stream is a self-contained JSON object. Just like with JSONL, these objects are individual, parsable JSON units. Let's take a look at how we might process this response in real time: ```bash curl https://api.openai.com/v1/responses \ -H "Content-Type: application/json" \ -H "Authorization: Bearer $OPENAI_API_KEY" \ -H "Accept: text/event-stream" \ --no-buffer \ --silent \ -d '{ "model": "gpt-4.1", "input": "Write a 200 word paragraph about tamborines.", "stream": true }' | stdbuf --output=L awk ' /response.output_text.delta/ { getline; if ($0 ~ /data:/) { gsub(/.*"delta":"/, "", $0); gsub(/".*/, "", $0); printf "%s", $0; fflush(); } }' ``` If you run this command (you may need to install `coreutils` if the `stdbuf` command is not found), you'll see the text being generated in real time, one chunk at a time. This is the power of streaming! But as you can see, dealing with the SSE wrapper adds some complexity. Now, what if your API wants to stream a sequence of JSON objects without the additional SSE framing? What if you just want to send one JSON object after another as effortlessly as possible? That's precisely where JSONL shines as the "simplest tool". ## Streaming with JSONL To see this in action, you can use your own Vercel project or deploy one of Vercel's boilerplate projects. For this example, we'll use the [Vercel AI Chatbot](https://github.com/vercel/ai-chatbot) project. ### Deploying the Vercel AI Chatbot First, let's deploy the AI Chatbot: - Log in to your Vercel account. - Click on the **New Project** button. - Select the **AI Chatbot** template. - Follow the prompts to deploy the project. ### Gathering your environment variables In Vercel, create a new token for your account. This token will be used to authenticate your API requests. - Go to your Vercel account settings. - Click on the **Tokens** tab. - Click **Create**. - Copy the token and save it in a secure place. Next, we need to get your Vercel project ID and deployment ID: - Go to your Vercel project dashboard. - Click on the **Settings** tab. - Copy the project ID from the URL and save it. - Now, click on the **Deployments** tab. - Click on the **Latest Deployment** link. - Copy the deployment ID from the URL (prefix it with `dpl_`). - Save the prefixed deployment ID in a secure place. ### Setting up your environment variables In your terminal, run the following command to set your environment variables: ```bash export VERCEL_TOKEN=your-vercel-token export VERCEL_PROJECT_ID=prj_your-vercel-project-id export VERCEL_DEPLOYMENT_ID=dpl_your-vercel-deployment-id ``` ### Streaming your logs Now that you have your environment variables set up, you can start streaming your Vercel logs. Run the following command in your terminal: ```bash curl https://api.vercel.com/v1/projects/$VERCEL_PROJECT_ID/deployments/$VERCEL_DEPLOYMENT_ID/runtime-logs \ -H "Authorization: Bearer $VERCEL_TOKEN" \ --no-buffer \ --silent ``` This starts streaming your Vercel logs in real time. You should see something like this: ```jsonl {"level":"info","message":"Starting Vercel runtime..."} {"level":"info","message":"Vercel runtime started."} {"level":"info","message":"Received request: GET /api/chat"} {"level":"info","message":"Processing request..."} {"level":"info","message":"Request processed."} {"level":"info","message":"Sending response..."} {"level":"info","message":"Response sent."} {"level":"info","message":"Request completed."} {"level":"info","message":"Vercel runtime stopped."} ``` Now this looks familiar, right? Each line is a complete JSON object, and you can process it line by line as it arrives. If you use your app, you should see the logs being streamed in real time. Now we can use [jq](https://stedolan.github.io/jq/) to filter the logs and extract the messages: ```bash curl https://api.vercel.com/v1/projects/$VERCEL_PROJECT_ID/deployments/$VERCEL_DEPLOYMENT_ID/runtime-logs \ -H "Authorization: Bearer $VERCEL_TOKEN" \ --no-buffer \ --silent | jq -r 'select(.level == "error") | .message' ``` This filters the logs and only shows the `error`-level messages. Using `jq` is a great way to process JSONL data in real time, and it works perfectly with streaming data. The output should look something like this: ```text ⨯ [Error: No response is returned from route handler '/vercel/path0/app/(chat)/api/chat/route.ts'. Ensure you return a `Response` or a `NextResponse` in all branches of your handler.] ⨯ [Error: No response is returned from route handler '/vercel/path0/app/(chat)/api/chat/route.ts'. Ensure you return a `Response` or a `NextResponse` in all branches of your handler.] ⨯ [Error: No response is returned from route handler '/vercel/path0/app/(chat)/api/chat/route.ts'. Ensure you return a `Response` or a `NextResponse` in all branches of your handler.] ``` This is way easier than trying to parse an entire JSON object with a complex structure. We're querying a stream of JSON objects with the simplest of tools in real time. ## Generating JSONL on the server side You've seen how powerful consuming JSONL streams can be, especially with tools like `jq`. But what about producing them from your own API? Good news: it's often simpler than constructing and managing large, in-memory JSON arrays. These are the core principles an API producer should follow when streaming JSONL: - **Set the correct headers:** Make sure to set the `Content-Type` header to `application/jsonl`. - **Iterate and Serialize:** Process your data record by record (for example, from a database query, an LLM token stream, or any other iterable source). Convert each record into a JSON string. - **Write, newline, flush, repeat:** Write the JSON string to the response output stream, follow it with a newline character (`\n`), and flush the output buffer. Flushing ensures the data is sent to the client immediately, rather than being held back by the server or application framework. ### Python example Let's illustrate these principles with a simple Python Flask example. Flask is a lightweight web framework that makes streaming straightforward. ```python filename="app.py" from flask import Flask, Response import json import time app = Flask(__name__) # Simulated data source (e.g., could be database records or LLM outputs) def generate_data_records(): """A generator function that simulates fetching data records one by one.""" records = [ {"id": 1, "event": "user_signup", "timestamp": "2025-05-21T10:15:00Z", "details": {"user_id": "u123", "method": "email"}}, {"id": 2, "event": "item_purchase", "timestamp": "2025-05-21T10:15:30Z", "details": {"item_id": "p456", "quantity": 2}}, {"id": 3, "event": "image_processed", "timestamp": "2025-05-21T10:16:00Z", "details": {"image_id": "i789", "status": "completed"}}, # Simulate a stream that continues for a bit {"id": 4, "event": "llm_token", "timestamp": "2025-05-21T10:16:05Z", "details": {"token": "Hello"}}, {"id": 5, "event": "llm_token", "timestamp": "2025-05-21T10:16:06Z", "details": {"token": " world"}}, {"id": 6, "event": "llm_token", "timestamp": "2025-05-21T10:16:07Z", "details": {"token": "!"}}, ] for record in records: yield record time.sleep(0.5) # Simulate some processing time or data arrival delay @app.route('/stream-data') def stream_data(): def generate_jsonl(): for record in generate_data_records(): # Serialize each record to a JSON string json_string = json.dumps(record) # Yield the JSON string followed by a newline yield json_string + '\n' # In many web frameworks, yielding from a generator response # handles flushing implicitly. For some WSGI servers or specific # setups, explicit flushing might be needed if data isn't # appearing immediately on the client. # Return a Flask Response object, using the generator. # Set the mimetype to application/jsonl. return Response(generate_jsonl(), mimetype='application/jsonl') if __name__ == '__main__': # For development: # In production, use a proper WSGI server like Gunicorn or uWSGI. # Example: gunicorn --workers 1 --threads 4 myapp:app app.run(debug=True, threaded=True) ``` To run this example: - Save it as a Python file (for example, `app.py`). - Install Flask using `pip install Flask`. - Run the app using `python app.py`. - In another terminal, use `curl` to access the stream: ``` curl -N http://127.0.0.1:5000/stream-data ``` **Note:** Using the `-N` or `--no-buffer` flag with curl is important for seeing the output as it arrives. - Watch as each JSON object is printed on a new line. The objects appear one by one, just as intended with JSONL. This server-side approach is memory efficient because you're not building a massive list of objects in memory. You process and send each one, then discard it from server memory (or at least, from the application's direct memory for that response). ### Handling errors mid-stream What if something goes wrong while you're generating the stream? One common practice with JSONL is to ensure that even error messages are valid JSON objects. You could, for example, have the last line of your stream be a JSON object indicating an error: ```jsonl { "status": "error", "code": "DB_TIMEOUT", "message": "Database query timed out while generating report." } ``` Clients can be designed to check the last line or look for objects with an `error` status. Since each line is independent, prior valid data may still be usable. ## Will this really make a difference? The difference between JSONL and traditional JSON is that JSONL allows you to start processing data as soon as it arrives, rather than requiring you to wait while the entire dataset is generated. Again, this is especially important in agentic applications or chat interfaces, where users expect to see results as soon as possible. This means we should aim for the lowest possible **time to first byte** (TTFB), instead of optimizing for throughput. Let's see how this looks in practice. ## Benchmarking JSONL vs traditional JSON To effectively compare the performance of JSONL and traditional JSON, we'll first modify our Python Flask application to include an endpoint that serves data in the traditional, full-JSON-response manner. This allows us to use the same data generation logic for both streaming (JSONL) and traditional responses. ### Modifying the Flask app for benchmarking We'll add a new route, `/traditional-data`, to our `app.py`. This route will first collect all data records into a list, then send them as a single JSON array. Here's the updated `app.py`: ```python filename="app.py" from flask import Flask, Response, jsonify # Added jsonify import json import time app = Flask(__name__) # Simulated data source (e.g., could be database records or LLM outputs) def generate_data_records(): """A generator function that simulates fetching data records one by one.""" records = [ {"id": 1, "event": "user_signup", "timestamp": "2025-05-21T10:15:00Z", "details": {"user_id": "u123", "method": "email"}}, {"id": 2, "event": "item_purchase", "timestamp": "2025-05-21T10:15:30Z", "details": {"item_id": "p456", "quantity": 2}}, {"id": 3, "event": "image_processed", "timestamp": "2025-05-21T10:16:00Z", "details": {"image_id": "i789", "status": "completed"}}, # Simulate a stream that continues for a bit {"id": 4, "event": "llm_token", "timestamp": "2025-05-21T10:16:05Z", "details": {"token": "Hello"}}, {"id": 5, "event": "llm_token", "timestamp": "2025-05-21T10:16:06Z", "details": {"token": " world"}}, {"id": 6, "event": "llm_token", "timestamp": "2025-05-21T10:16:07Z", "details": {"token": "!"}}, ] for record in records: yield record # Crucial: Simulate work/delay for each record # This makes the TTFB difference apparent in benchmarks time.sleep(0.5) @app.route('/stream-data') # Our existing JSONL streaming endpoint def stream_data(): def generate_jsonl(): for record in generate_data_records(): json_string = json.dumps(record) yield json_string + '\n' return Response(generate_jsonl(), mimetype='application/jsonl') @app.route('/traditional-data') # New endpoint for traditional JSON def traditional_data(): all_records = [] for record in generate_data_records(): all_records.append(record) # Sends the entire list as a single JSON array response return jsonify(all_records) if __name__ == '__main__': app.run(debug=True, threaded=True) # threaded=True can help with concurrent requests during testing ``` Ensure this updated `app.py` is running. If it was already running from the previous section, it will most likely auto-reload, but you might need to stop and restart it to activate the new `/traditional-data` route. ### Running the benchmark We'll use `curl` along with its `-w` (write-out) option to capture specific timing information, especially `time_starttransfer`, which is our TTFB. The `time` utility (often a shell built-in command) will measure the total wall-clock time for the `curl` command. Open your terminal (while `app.py` is running in another) and execute the following commands to measure the TTFB and total time. For JSONL streaming: ```bash time curl -N -s -o /dev/null -w "TTFB (JSONL Stream): %{time_starttransfer} s\nTotal time (curl JSONL): %{time_total} s\n" http://127.0.0.1:5000/stream-data ``` For traditional JSON: ```bash time curl -s -o /dev/null -w "TTFB (Traditional JSON): %{time_starttransfer} s\nTotal time (curl Traditional): %{time_total} s\n" http://127.0.0.1:5000/traditional-data ``` In these code blocks: - The `-N` or `--no-buffer` option for `curl` disables buffering of the output stream, allowing us to see streaming behavior. - The `-s` option silences progress output from `curl`. - The `-o /dev/null` option discards the actual response body so it doesn't clutter the terminal (we're interested in timings here). - The `-w "..."` option formats the output from `curl` after the transfer: - `%{time_starttransfer}` is the TTFB. - `%{time_total}` is the total transaction time measured by `curl` itself. - The `time` command prefixing `curl` gives an overall execution time from the shell's perspective. ### Results In our tests, we saw the following results: | | TTFB | Total time | | ---------------- | ---------- | ---------- | | curl JSONL | 0.001550 s | 3.021876 s | | curl traditional | 3.018902 s | 3.019132 s | ## What about subjective observations? We've seen the numbers, but we know that the perception of something as "fast" or "slow" is subjective. So let's look at the qualitative differences in how the data arrives. ### Observing the stream To visually see the difference in how the data arrives, run `curl` without discarding the output. - For JSONL streaming: ```bash curl -N http://127.0.0.1:5000/stream-data ``` You'll see the data appear line by line. Each JSON line prints about half a second after the previous line. - For traditional JSON: ```bash curl http://127.0.0.1:5000/traditional-data ``` You'll see there is a pause of about three seconds, then the entire JSON array is printed at once. ## Interpreting the benchmark The results should clearly demonstrate: - **Vastly superior TTFB for JSONL:** This is the most critical takeaway. JSONL allows the server to send initial data almost immediately after it's ready, significantly improving perceived performance and enabling real-time updates for the client. In our simulation, this is because the first `time.sleep(0.5)` elapses, one record is yielded, and Flask sends it. - **Client-side processing can begin sooner with JSONL:** Because each line is a complete JSON object, the client can start parsing and using the data as it arrives. With traditional JSON, the client must wait for the entire payload. - **JSONL improves server-side memory efficiency (implied):** While this benchmark doesn't directly measure server memory, recall that the `/stream-data` endpoint processes one record at a time, while `/traditional-data` builds a list of `all_records` in memory. For large datasets, this difference is important for server stability and scalability. Our example dataset is small, but extrapolate this to millions of records, and the traditional approach becomes infeasible or very resource-intensive. The `time.sleep(0.5)` in `generate_data_records` is key to this demonstration. It simulates the real-world scenario where records aren't all available instantaneously (for example, they may be results from an LLM generating tokens, database queries, or other microservice calls). Without such a delay in a local benchmark against a very fast data generator, the TTFB for both might appear small and hide the architectural benefit. This benchmark should provide compelling evidence that when TTFB matters, JSONL is a significantly better method than waiting for a complete JSON dataset. ## How Speakeasy enables API producers to stream responses Speakeasy SDKs include built-in JSONL streaming support. To enable this for your API: 1. Set the response content type to `application/jsonl` in your OpenAPI spec 2. Speakeasy generates SDK code that handles the streaming automatically 3. Your API/SDK users get clean, easy-to-use streaming methods Here's an example of how to set the `application/jsonl` content type in your OpenAPI document: ```yaml paths: /logs: get: summary: Stream log events operationId: stream tags: - logs parameters: - name: query in: query required: true schema: type: string responses: "200": description: Log events stream content: # Either content type can be used: application/jsonl: schema: $ref: "#/components/schemas/LogEvent" # OR application/x-ndjson: schema: $ref: "#/components/schemas/LogEvent" components: schemas: LogEvent: description: A log event in line-delimited JSON format type: object properties: timestamp: type: string message: type: string ``` See our documentation on [enabling JSON lines responses](https://www.speakeasy.com/docs/customize/runtime/jsonl-events) for detailed information about how best to prepare your OpenAPI document for streaming JSONL responses. Speakeasy also generates the necessary code for SSE, so your users don't need to worry about the underlying implementation details. They can focus on building their applications and consuming your API. From your user's perspective, SDK implementations of your API will look like this: ```python filename="example.py" from speakeasy_sdk import SDK with SDK() as sdk: stream = sdk.analytics.stream( start_date="2024-01-01" ) with stream.object as jsonl_stream: for event in jsonl_stream: # Handle the event print(event, flush=True) ``` We hope this guide has shown you that streaming JSONL responses is not only possible, but simple and efficient. By using JSONL, you can provide a better user experience, reduce server load, and make your API more responsive. # Why work on API developer experience? Source: https://speakeasy.com/blog/why-work-at-speakeasy As the end of 2023 quickly approaches my internal calendar is reminding me of the natural slow down that happens. However on the ground reality at Speakeasy has not matched that expectation at all. December is shaping up to be our busiest month of the year! The holidays though have given me a moment to reflect on what we do and why we do it. There is a lot happening in the tech world right now. The Generative AI boom is in full swing. It feels like it's never been a more exciting time to be a builder. Large language models are making new business and service creation less capital intensive. Natural language everything means an explosion of new products and a compression of the time to market. This also means it's an incredibly challenging time to be a builder. The number of frameworks and tools is exploding. The number of apis is exploding. The burden of integration is exploding. Whether you're bootstrapping a startup, building at a scaleup, or innovating at a large enterprise, the number of choices you have to make is exploding. If you haven't caught my vibe yet it's really :exploding_head: out there! In the middle of all that noise the best companies have been able to clarify against competition by investing in community. They've built loyal developer user groups by getting the details right. This is the fine points of day to day integration work that most don't obsess over. This is developer experience. Specifically API developer experience. For me this is best captured by Twillio's sign off of the 101 freeway in San Francisco. "Ask your Developer". Not a question, not an answer. Just a statement with immense gravity and implication. Developer communities are loyal to great experiences. They're loyal to great products. Most companies fall short on this vision. To get it right you have to burn several percentage points of company time purely on conviction. ![Twilio billboard off the 101 - Ask your developer](/assets/blog/why-work-at-speakeasy/ask-your-developer.jpeg) Working at Speakeasy means working on the brass tacks. We're in the weeds of the experiences that best in class products truly standout. We sweat the details so our customers don't have to. At its core today we're a unique code generation platform that takes massive burden off API devs, teams and companies from worrying about staying up to date with every developer ecosystem. As an API company having a great SDK may seem like a narrow start. But it's the gateway drug to a suite of tooling that gives your customers the fabled "aha" moment and keeps them coming back for more. API first is out. SDK first is in. This isn't always flashy work. Through APIs of our customers we get to look at the guts of many different businesses across many verticals and then wrap those into easy to understand experiences for millions of end users. Whether its Generative AI, Fintech, Infrastructure or something else we're giving a set of high leverage tools for builders to reach their customers in the best way possible. Our mission and story doesn't end with developers. For the GTM half of businesses we ensure you never have to turn down a deal or risk customer success because of a lack or resources or prioritisation. The modern business is a balancing act between great product build and great distribution. We reduce that tension by taking the burden of distribution to your developer base. Speakeasy is a very ambitious project. We're building a company/product/team that will power the next generation of APIs and developer adoption at the fastest growing growing companies. REST API best practices may already have broad consensus but they definitely don't have broad practice. We have to meet our customers where they are. Sometimes this means APIs being designed from the ground up and sometimes it means ones that support many 0's of RPS. This means we have to build extensibly, reliably and at scale. We have the privilege of sitting between producers and consumers and shepherding usage in a way that dictates the exact lines of code used. So very design decision we make can future proof our customers or cut their growth short. It's high stakes which also means it's a lot of fun :). We're working on Speakeasy because the best businesses are increasingly being built on a community of developers. We ship fast so our customers can ship fast and keep their customers happy. We have a lot of work to do to bring this reality to every business, it's going to a be an exciting 2024. If you're excited by our mandate check out our [open roles](https://jobs.ashbyhq.com/Speakeasy) - we'd love to hear from you! # Why we list Zod as a dependency and not as a peer dependency Source: https://speakeasy.com/blog/why-zod-is-a-dependency import { Callout } from "@/mdx/components"; Speakeasy-generated TypeScript SDKs use Zod for runtime validation to ensure correctness, provide safer defaults, and maintain type safety between API responses and TypeScript types. This validation layer catches mismatches between the OpenAPI specification and actual API behavior, preventing runtime errors in production. Speakeasy includes `zod` as a regular dependency rather than a peer dependency. This design decision ensures better compatibility, simpler installation, and more reliable SDK behavior across different project configurations. ## Understanding peer dependencies [Peer dependencies are designed to indicate that a package is a plugin or extension on top of another package](https://nodejs.org/es/blog/npm/peer-dependencies/). Quoting from the article, "a plugin package is meant to be used with another "host" package, even though it does not always directly use the host package". In other words, peer dependency indicates this package should be not installed if you don't already use "package A" in your project. For example, a Babel plugin should be listed as a peer dependency because it makes no sense to install a Babel plugin if Babel isn't already in the project. Similarly, `zod-to-openapi` lists Zod as a peer dependency because it's a plugin that extends Zod's functionality. ## Why Speakeasy SDKs use dependencies Speakeasy SDKs are not plugins on top of Zod. They are standalone libraries that use Zod internally for runtime validation. It makes perfect sense to install and use a Speakeasy SDK even if the project doesn't otherwise use Zod. ### Practical benefits Whether using dependencies or peerDependencies, if an existing compatible version exists, npm will deduplicate installations. Also, starting with npm 7, npm automatically attempts to install peer dependencies. In many projects there's little practical difference between `dependencies` and `peerDependencies`. However, in edge cases `peerDependencies` will result in confusing errors and incompatibility with certain projects: #### Backward compatibility If a project uses an older legacy version of Zod which is incompatible with our SDKs (such as Zod v2), the SDK will still work correctly with its own compatible Zod version installed. With a peerDependency, the SDK would be forced to use the project's older Zod version, which would break the SDK due to compatibility issues and preventing installation entirely. #### Forward compatibility If a project starts using Zod v5 in the future, and the SDK isn't yet compatible with Zod v5, the SDK will continue to work correctly. A compatible version of Zod will be installed alongside the project's Zod v5, and the SDK will use its own version. With a peerDependency, the SDK would attempt to use the project's Zod v5, potentially causing runtime errors if there are breaking changes. #### No version conflicts When multiple packages list React as a peerDependency with conflicting version requirements, npm throws an error because having multiple React versions in the same application causes problems. At time of writing, `pnpm` will just pick one of the versions and probably result in runtime issues. However, in our case, having multiple Zod versions in a project is not problematic. Different packages can use different Zod versions without interfering with each other, since they don't share global state or interact at runtime. #### Simplified installation Peer dependencies must be installed separately when using Yarn or earlier npm versions. With Zod as a regular dependency, SDK installation is a single step with no additional configuration required. ## Version resolution Whether Zod is listed as a `dependency` or `peerDependency`, if a compatible version of Zod is already installed in the project, that version will be used. Modern package managers deduplicate dependencies when possible, so listing Zod as a regular dependency doesn't result in unnecessary duplication. We aim to make our SDKs compatible with as broad a range of possible Zod versions to leverage this deduplication. Listing Zod as a dependency rather than a peerDependency provides better compatibility, simpler installation, and more reliable SDK behavior while maintaining the same deduplication benefits. # Rest of gen.yaml omitted Source: https://speakeasy.com/blog/windows-support-quickstart-and-a-v1-sunset import { Callout } from "@/lib/mdx/components"; Before everyone dissipates into the BBQ smoke of the summer holidays, we've got a few more updates to share. This week, amid the usual slew of improvements and fixes, we're excited to announce the release of Windows support, a smarter Quickstart, and the coming sunset TypeScript V1. Let's get into it 👇 ## Full CLI Support for Windows With last month's [GA release of our C# SDK generation](/changelog/changelog-2024-05-01), it's as good a time as any to make sure that the CLI is as easy to use on Windows as it is on MacOS and Linux. And so we're happy to announce that Windows is now a fully supported platform. We've made sure that our CLI's interactive mode works seamlessly, and our CLI's integration testing has been updated to include Windows environments. Install our CLI on Windows: ```bash choco install speakeasy ``` ## A Quicker Quickstart ```bash │ Workflow - success │ └─Target: my-first-target - success │ └─Source: openapi - success │ └─Tracking OpenAPI Changes - success │ └─Snapshotting OpenAPI Revision - success │ └─Storing OpenAPI Revision - success │ └─Validating Document - success │ └─Retrying with minimum viable document - success │ └─Source: openapi - success │ └─Applying Overlays - success │ └─Apply 1 overlay(s) - success │ └─Tracking OpenAPI Changes - success │ └─Snapshotting OpenAPI Revision - success │ └─Storing OpenAPI Revision - success │ └─Validating Document - success │ └─Validating gen.yaml - success │ └─Generating Typescript SDK - success │ └─Setup Environment - success │ └─Load and Validate Document - success │ └─Generate SDK - success │ └─Compile SDK - success │ └─Cleaning up - success ``` We would love to think that every OpenAPI spec is error-free, but we know that not every company is using [our linter](/docs/linting/linting) (yet). That's why we've rebuilt Speakeasy Quickstart to be able to persevere through errors to help teams get to a minimally viable SDK faster. Instead of blocking generation when there's an error in the OpenAPI syntax, `quickstart` will pare down your spec to the validly documented operations and generate an SDK with just those. The error logs will be logged separately so that you can go back and make your fixes. Just another small change in the name of getting users their SDKs sooner. ## TypeScript V1 Rides Off Into The Sunset Just as important as the new products you roll out, are the old products you retire. And so, we're officially announcing the sunset of TypeScript V1 on **August 9th, 2024**. We [rolled out TypeScript v2 in December 2023](/post/introducing-universal-ts), and since then, we've been working to make sure that all of our users have had the time to migrate. We're at almost 100% migration, and so we're ready to say goodbye to TypeScript V1. If anyone needs reminding of the benefits of switching, here's the summary of the changes: - Dependencies: v1 used Axios; v2 uses the Fetch API. - Compatibility: v2 supports Node.js, Deno, Bun, and React Native. - Validation: v2 integrates Zod for robust runtime data validation. - Polymorphic Types: v2 handles complex API schemas better. Switching is as easy as a 1 line change in your SDK's `gen.yaml` file: ```yaml filename="gen.yaml" configVersion: 1.0.0 typescript: templateVersion: v2 ``` --- ## 🚢 Improvements and Bug Fixes 🐛 Based on the most recent CLI version: [**Speakeasy v1.309.1**](https://github.com/speakeasy-api/speakeasy/releases/tag/v1.309.1) ### The Platform 🐛 Fix: Handle edge case where `.yaml` looks like `.json`\ 🐛 Fix: Handle empty responses in webhooks ### Typescript 🐛 Fix: Added explicit types for exported enum schemas ### Terraform 🐛 Fix: Edge case with combination of oneOf and non-oneOf in TF ### Java 🚢 Feat: Add support for client credentials to java \ 🚢 Feat: Support user customization of `build.gradle` \ 🐛 Fix: Addressed compiler errors for pagination-enabled methods that exceed `maxMethodParameters` ### C# 🚢 Feat: `Nuget.md` file is generated for published C# packages \ 🐛 Fix: Handle missing C# imports for unions ### Unity 🐛 Fix: Address bugs related to Unity's support for only `.Net5` # writing-zod-code-that-minifies Source: https://speakeasy.com/blog/writing-zod-code-that-minifies I don't think it's exaggerating when I say that [Zod][zod] has had a major positive impact on how we write safe TypeScript code that also preserves its safety guarantees at runtime. It's so powerful that we at Speakeasy based the design of our [latest TypeScript SDK generator](/post/introducing-universal-ts) on it. We've been iterating on the TypeScript code we're generating because we're going through a phase of optimising the size of our SDKs so they work even better in the browser and one interesting area has been around how we write Zod schemas. Consider the following (contrived) example of a Zod schema: ```ts import * as z from "zod"; export const person = z.object({ name: z.string(), age: z.number().optional(), address: z.object({ line1: z.string().optional(), line2: z.string().optional(), line3: z.string().optional(), line4: z.string().optional(), city: z.string(), }), pets: z.array( z.union([ z.object({ type: z.literal("dog"), name: z.string(), }), z.object({ type: z.literal("cat"), name: z.string(), }), ]) ), }); ``` This is a fairly common style of writing zod schemas: you import the whole library as a single variable and use the powerful chaining API to describe validation rules. When running this code through a bundler like esbuild we're going to get subpar minification performance. Esbuild will remove unnecessary spaces and newlines but I'm going to keep them in for the sake of readability. Here's the formatted result: ```ts import * as t from "zod"; export const person = t.object({ name: t.string(), age: t.number().optional(), address: t.object({ line1: t.string().optional(), line2: t.string().optional(), line3: t.string().optional(), line4: t.string().optional(), city: t.string(), }), pets: t.array( t.union([ t.object({ type: t.literal("dog"), name: t.string() }), t.object({ type: t.literal("cat"), name: t.string() }), ]) ), }); ``` The actual minified code comes out to 379 bytes, down from 512 bytes. That's rougly a 26% reduction. Notice how not a lot has changed. In fact, the only thing that has changed is that esbuild renamed `z` to `t`. The only net reduction in this minified code can be attributed to the removal of unnecessary whitespace characters which we've kept in here. So on the current course, if your project is building up more and more Zod schemas, you'll notice that the minified code isn't tremendously smaller than the unminified code. The only real gains will be from compressing this code with gzip or brotli (or whatever you prefer) but that doesn't impact the size of the code that needs to be parsed. Furthermore, minified can still compress well and result in overall less text to send down the wire and to parse. The way to improve the impact of minification on our code will come from using local variables that can be rewritten by the minifier. Fortunately, Zod exports much of its API as a standalone functions that can be separately imported. Lets rewrite the code above using these standalone functions: ```ts import { object, string, number, array, union, literal, optional } from "zod"; export const person = object({ name: string(), age: optional(number()), address: object({ line1: optional(string()), line2: optional(string()), line3: optional(string()), line4: optional(string()), city: string(), }), pets: array( union([ object({ type: literal("dog"), name: string(), }), object({ type: literal("cat"), name: string(), }), ]) ), }); ``` ... and the minified result: ```ts import { object as t, string as e, number as i, array as o, union as r, literal as a, optional as n, } from "zod"; export const person = t({ name: e(), age: n(i()), address: t({ line1: n(e()), line2: n(e()), line3: n(e()), line4: n(e()), city: e(), }), pets: o( r([t({ type: a("dog"), name: e() }), t({ type: a("cat"), name: e() })]) ), }); ``` That did better. The actual result is now 290 bytes, down from 512 bytes. That's roughly a 43% reduction! You can see how the minifier was able to re-alias all the imports to single letter variables and use that to shrink the remaining code. The new style of writing Zod schemas might be a little more tedious because you are no longer carrying the entire library with you with a single variable. However, if optimising for bundle size is a real concern for you, then this is a neat trick to keep in your backpocket. [tsv2]: (/post/introducing-universal-ts) [zod]: https://zod.dev/ # airbyte Source: https://speakeasy.com/customers/airbyte import { Card, Table } from "@/mdx/components"; ## Company Info - **Company:** Airbyte - **Industry:** Data Integration - **Website:** [airbyte.com](https://airbyte.com/) ## Key Takeaways - Unlocked access to the Terraform community - ~500 Terraform resources maintained automatically - Huge improvement in time-to-market: from 9 months to 4 weeks --- ## Offering a Terraform Provider and SDKs Accelerates Adoption and Improves Customer Experience [Airbyte](https://airbyte.com/) (W'20 YC) is an open-source data integration platform, syncing data from applications, APIs & databases to destinations such as data warehouses, data lakes & databases. Their developer-first approach has led to a robust and loyal community, with 40,000+ users and over [⭐ 10K on Github](https://github.com/airbytehq/airbyte). The Airbyte API enables customers to programmatically control the Airbyte platform. For example, users can easily create a new source, update a destination, and change data sync frequency. Thanks to Speakeasy SDKs, Airbyte's customers can rapidly integrate with the API, without needing to read through pages of docs, or write and debug a lot of boilerplate code. Customers simply import the SDK and call the relevant methods. In addition, Speakeasy also creates and maintains a Terraform provider for the Airbyte API. By allowing customers to control Airbyte through Terraform they enable customers to manage their "Infrastructure as Code" and have a superior product experience. For customers, this enables: - Rapid deployment and configuration - Version control and collaboration of Terraform configuration - Automated deployment and scaling across different environments - More robust deployments, with reduced errors thanks to explicit and declarative configuration Offering a Terraform provider is also a compelling strategic move for Airbyte: - Unlocking access to the Terraform community - Streamlined customer onboarding - Greater competitive differentiation - Increasing product stickiness by embedding Airbyte within a company's infrastructure ## Speakeasy Uses Existing OpenAPI Spec to Minimize Eng Burden Creating and maintaining SDKs and Terraform providers can be a huge lift for engineering and product teams — particularly at the scale at which Airbyte is operating: Speakeasy offers a dramatically simpler approach: There is also a huge ongoing saving in terms of cost and developer toil: We're excited to partner with the Airbyte team, to make an amazing modern data platform even better for its users. ---
# apex Source: https://speakeasy.com/customers/apex import { YouTube, Card, Table } from "@/mdx/components"; ## Company Info - **Company:** Apex Fintech Solutions - **Industry:** FinTech - **Website:** [apexfintechsolutions.com](https://apexfintechsolutions.com/) [Apex Fintech Solutions](https://apexfintechsolutions.com/) offers a range of services for the fintech industry, trusted by companies like Ally (traded on NYSE), SoFi, and many more. With $123B+ in assets under custody, they enable modern investing and wealth management tools through frictionless platforms, APIs, and other services. ## APIs Power Successful Fintechs APIs play a crucial role for Apex and any modern financial institution. Financial institutions have historically relied on outdated practices to deliver consumer experiences, such as wet signatures for authorization, cumbersome batch processing of transactions, and error-prone manual data entry. These practices led to high cost-to-serve, long transaction processing times, and reduced customer satisfaction. By offering services via APIs, Apex empowers its financial services customers to eliminate these inefficiencies, reduce costs, and accelerate transaction speed. Thanks to these APIs, companies like eToro can offer compelling consumer experiences like real-time $0 commission trades. ## Challenges Apex Wanted to Solve Due to the critical importance of APIs for Apex's business, a fast and seamless integration experience is essential. The Apex team identified numerous ways that Speakeasy could help: - **Streamline integrations and save time for their customers:** Apex noticed that about ~30% of each customer's integration code was the same, and had to be unnecessarily re-written from scratch by every customer. By providing this code out-of-the-box in a client SDK, Apex realized they could help customers get to a live integration faster while significantly reducing the potential for coding bugs. - **Ensure stability of services:** Placing the burden of writing the entire integration on the customer alone exposes them to potential errors, such as forgetting to account for pagination, retries, error handling, or dealing with auth in an insecure manner. These issues can cause the integration to fail unexpectedly, which would reflect poorly on Apex. Handling these concerns in an SDK enabled Apex to better ensure that integrations are built optimally and reliably, ultimately leading to improved customer satisfaction and reduced support costs. - **Easily handle breaking change mitigation:** As their APIs evolve, Apex wanted to offer customers a better way to update their integrations. --- ## Speakeasy SDKs Enable API Integration Excellence The team chose the Speakeasy platform to generate SDKs that would allow them to achieve these goals and provide integration guidance to their customers. Instead of expecting the customers to build the integration on their own, in their own way, with potential errors, Apex empowers them with all the necessary tools to minimize the effort on the customer side.
Through strategic collaboration with Speakeasy, Apex Fintech Solutions has overcome integration hurdles, paving the way for enhanced customer experiences and accelerated growth. By empowering customers with user-friendly SDKs and comprehensive documentation, Apex reinforces its position as a leader in the fintech landscape, driving innovation and enabling seamless digital transformation. ## Key Takeaways How Apex Fintech Solutions benefits from using Speakeasy to generate SDKs for their API platform: ### Standardized Integration Experience Apex eliminated 30% of redundant integration code for their customers, enabling faster and more reliable connections. ### Improved Service Reliability By handling common integration challenges like pagination, auth, and error handling within SDKs, Apex reduced integration failures and support costs. ### Cost-Effective Maintenance Automating SDK generation with Speakeasy allowed Apex to support multiple programming languages without hiring specialized engineering teams. # apiture Source: https://speakeasy.com/customers/apiture import { Card, Callout, CodeWithTabs, Table, YouTube } from "@/mdx/components"; ## Company Info - **Company:** Apiture - **Industry:** FinTech - **Website:** [apiture.com](https://www.apiture.com) ## Overview: Modern Banking Solutions Powered by APIs Nowhere has the digital revolution been as furiously fast-paced as the financial ecosystem. To help banks and credit unions evolve with this ever-changing ecosystem, [Apiture](https://www.apiture.com/) provides online and mobile banking solutions. The key to their approach lies in the name: Apiture. All of Apiture's solutions rely on the Apiture API platform. That's why Chief API Officer, David Biesack, is always on the hunt for the best API technology available to his team, and why he was early to partner with Speakeasy to develop SDKs. ## Challenge: Incomplete Support & Maintenance Burden of OSS Generators Prior to Speakeasy, Apiture had a custom SDK generation solution built using the OSS Generators. However, they were always acutely aware of the limitations:
Working with Speakeasy not only offered superior SDKs to what they were able to develop internally but also provided the Apiture team with a partner who cared as much about APIs as they did. --- ## Solution: Making Your API a Joy to Work With The Apiture team now has SDKs that cover the things they care about most, exposed in an interface that's easy to work with: ### Key Improvements with Speakeasy - **Comprehensive idiomatic error handling** to simplify the API user experience - **Easy-to-use authentication** to make APIs more accessible - **Dynamic runtime error checking** to ensure client & server code is always correct - **A CLI-native experience** to embed SDK creation in Apiture's CI/CD pipeline
The job of removing friction from your API is never finished, but using Speakeasy provides Apiture with tools that brings them one step closer to a seamless integration experience. --- ## Results: Enhanced Developer Experience and Support By partnering with Speakeasy, Apiture has been able to significantly improve both their internal development processes and the experience for developers consuming their APIs: ### Internal Benefits - **Reduced maintenance burden**: No longer spending engineering resources on maintaining custom templates - **Improved API discipline**: Better OpenAPI specs lead to better documentation and better SDKs - **CI/CD integration**: Automated SDK generation as part of development workflow - **Expert support**: Access to a team that specializes in SDK generation and OpenAPI ### External Benefits - **Higher quality SDKs**: More idiomatic code for developer consumers - **Complete feature coverage**: Full support for all API capabilities - **Better developer experience**: Type safety, error handling, and authentication made simple - **Streamlined integration**: Reduced friction for API consumers ## Looking Forward Apiture continues to work with Speakeasy to refine and improve their SDK offerings. The partnership ensures that as their API platform evolves, their SDK experience keeps pace, providing their banking customers with the tools they need to integrate quickly and effectively. As financial APIs continue to grow in complexity and importance, having a reliable SDK generation partner allows Apiture to focus on their core strength – building best-in-class banking solutions – while ensuring their developer experience remains best-in-class. # autobound Source: https://speakeasy.com/customers/autobound import { Card } from "@/mdx/components"; In the world of sales & marketing, personalization is king. [Autobound.ai](https://www.autobound.ai) enables teams to create hyper-targeted content at scale by combining 350+ data sources. However, leveraging mountains of data presents it's own problems. Problems that Autobound solved with an MCP server built on [Gram](/docs/gram/introduction). ## The limits of determinism For every prospect, Autobound's system has anywhere from 20 to 100 relevant insights available, from SEC filings to podcast appearances to LinkedIn activity. The challenge is figuring out which insights to draw on for each unique sales conversation, dynamically fetching additional context based on how the conversation progresses. That's a problem that required moving beyond deterministic algorithms into truly agentic decisioning. The algorithm could pick from available insights, but it couldn't reason about them. For that, Autobound needed MCP. ## MCP in days not weeks What Autobound wanted was straightforward: expose the core of their Insights API through an MCP server so that their [Vellum-based agents](https://www.vellum.ai/products/orchestration) could dynamically fetch insights based on context. Daniel Wiener, CEO of Autobound.ai, suspected that building it wouldn't be quite so straightforward. Gram changed the equation entirely. Autobound was able to easily transform their Insights API into a set of tools and deploy them as an MCP server. ## Pushing personalization forward with MCP After integrating Vellum's agents with their Gram-powered MCP server, something fundamentally different became possible. Now the agent determines how to structure tool calls based on context. It analyzes the returned insights and reasons about what additional data might improve content quality. It makes subsequent calls dynamically based on its reasoning, and Gram's self-healing properties ensure that tool calls have a high success rate. For a company whose core value proposition is having "the best insights to write the best content," this shift from deterministic selection to intelligent orchestration represents a fundamental evolution in how they deliver on that promise. ## The reality of production scale Getting an MCP server working in development is one thing. Scaling it to production volumes is another challenge entirely. Every email generated requires 5-10 tool calls through the MCP server to fetch and orchestrate insights. At full scale, that translates to millions of API interactions daily, all needing to happen fast enough to feel instant to sales teams working through their outreach sequences. This kind of scale exposes issues that never surface in testing. Connection pooling becomes critical. Error handling needs to be bulletproof, a failed tool call can't break an entire email generation flow. Latency matters when you're chaining multiple API requests together. And all of this needs to work reliably while Autobound's agents are dynamically deciding which subsequent requests to make based on previous results. The partnership with Speakeasy became essential here. Autobound stayed focused on tool & prompt design while Speakeasy handles the infrastructure to ensure the MCP server scales appropriately. ## Going live and looking ahead Autobound's first version went live within days of starting with Gram. The full rollout is planned over the coming weeks as they continue to refine their insight selection logic and scale up volume. The focus now is on monitoring the results. Does intelligent orchestration actually deliver better insights? Does better insight selection lead to better content? The directional indicators suggest yes, but Autobound is taking the time to measure properly. But the fundamental transformation is already clear. Autobound moved from a static algorithm to an intelligent reasoning system without blocking their engineering team or breaking their economics at scale. They went from exploring MCP through failed ChatGPT suggestions to production deployment in weeks, not months. Learn more about building production MCP servers: - [Gram Documentation](/docs/gram/introduction) # codat Source: https://speakeasy.com/customers/codat import { Card, Callout, CodeWithTabs, Table } from "@/mdx/components"; ## Company Info - **Company:** Codat - **Industry:** FinTech - **Website:** [codat.io](https://www.codat.io) ## Key Takeaways - "Three production-ready SDKs in < 2 weeks -- accelerating time-to-market by months", - "No need to hire dedicated eng team to maintain SDKs", - "SDKs used by internal teams to boost developer productivity" --- ## Challenge [Codat](https://www.codat.io) is the universal API for small business data. Intended for companies building software used by SMBs, the Codat API can be used for such varied purposes as underwriting credit risk, automating accounting, or building SMB dashboards. They serve as a one-stop shop for accessing consented business data from a customer's banking, accounting, and commerce platforms. Max Clayton Clowes and David Coplowe are part of Codat's developer experience SWAT team; a small team, with a big mandate. And in the beginning of 2023, they asked themselves a question that many a small DevEx team has obsessed over: what is the most impactful thing we could do to reduce our user's time to build? With the company being founded by developers, developer experience has always been at the heart of what they build. As such, Codat's dedicated DevEx team is relentlessly focused on ensuring Codat's implementation is simple and the time to integration (ie. time to 200) is measured in minutes. In the DevEx team's review, they identified SDKs as a top priority feature to improve the time to 200 for new users. SDKs had been a long-standing request from some of their largest customers, and could be a big plus for reducing the effort for Codat to onboard smaller customers. ### Languages identification They first identified which languages were important for them to support. In order of priority: - [C#](https://github.com/codatio/client-sdk-csharp) - [Typescript](https://github.com/codatio/client-sdk-typescript) - [Python](https://github.com/codatio/client-sdk-python) - [Go](https://github.com/codatio/client-sdk-go) Their plan was to attack the languages one at a time, starting with the languages which they had familiarity with: Go. But before they started coding, they first needed to decide on whether they were going to use a generator, or build the libraries by hand. From a time to value perspective, a generator was appealing. Especially for languages where they lacked expertise, but it would mean painful tradeoffs in terms of the vision they had for their SDKs. Ease of use and good developer ergonomics was extremely important to them. In particular they wanted an SDK with: - **Per Service Packages** - They wanted each Codat API e.g. banking, accounting, commerce to be available as a standalone package so that users could avoid bloat, and just grab what they need. - **Type Safety** - They wanted strongly typed library so that users could catch errors at compile time, and take advantage of their IDE's auto-completion. - **Appropriate Abstraction** - They wanted the methods in their SDK to abstract away API details like whether a parameter belongs in the path or the query. They wanted users to be able to focus on inputs & outputs. - **Accurate Documentation** - They wanted client libraries to be well-documented, with rich usage examples that would help customers get started. --- ## Solution Staring down the barrel of a trade-off between quality and speed, Codat went searching for alternative options. And that's when they found Speakeasy. Speakeasy enabled Max & David to easily create SDKs which met all of the important criteria for good ergonomics — done automatically from the Codat OpenAPI spec. Not only did the Speakeasy SDKs help deliver great developer ergonomics, but the managed SDK workflow made ongoing support trivial. In particular Speakeasy's OpenAPI validation has helped them make ongoing improvements to their OpenAPI spec, which has paid dividends not just in the creation of SDKs, but in improving their API reference documentation as well. --- ## Results
[Codat's SDKs](https://docs.codat.io/get-started/libraries) are available via their user's favorite package managers. The response has been overwhelmingly positive. Users have been delighted to have the option of integrating Codat using their preferred language and runtime. In addition, the SDKs have had a positive impact on internal development. Codat's application teams have received a boost in productivity as they've been able to take advantage of the SDKs to handle the more mundane aspects of integrating with backend services. [Take a closer at Codat's SDKs here](https://docs.codat.io/get-started/libraries). ### Benefits for Codat's Customers - **Shorter Integration Times**: Customers can start building with Codat APIs much faster using their preferred language - **Better Developer Experience**: Type safety, IDE auto-completion, and well-documented methods make development more efficient - **Reduced Overhead**: Abstraction of API details lets developers focus on their business logic instead of API mechanics ### Internal Benefits for Codat - **Accelerated Roadmap**: Delivered a major DevEx initiative in weeks instead of months - **Resource Efficiency**: No need to hire language specialists or dedicate engineering resources to SDK maintenance - **Enhanced Productivity**: Internal teams use the SDKs to streamline their own integrations with Codat services - **Improved Documentation**: OpenAPI validation led to improvements in both SDK usability and API reference documentation --- ## SDK Adoption Codat continues to expand their SDK offerings with consistent updates as their API evolves. The Speakeasy platform ensures that as Codat adds new capabilities, their SDKs automatically stay synchronized with their latest API features.
Each package is modular, allowing customers to install only the components they need, resulting in more efficient integration experiences. Visit [Codat's documentation](https://docs.codat.io/get-started/libraries) to explore their full SDK offerings. # conductorone Source: https://speakeasy.com/customers/conductorone import { Card, Table } from "@/mdx/components"; ## Company Info - **Company:** ConductorOne - **Industry:** Identity Security - **Website:** [conductorone.com](https://www.conductorone.com/) ## Key Takeaways - Go, Python, Typescript SDKs and a Terraform provider launched in weeks, not months - 650 hours of upfront development saved -- plus ongoing maintenance - Fully automated pipeline: from spec all the way through to package manager --- Founded in 2020, [ConductorOne](https://www.conductorone.com/) is dedicated to providing secure identity for the modern workforce. Companies today suffer from identity sprawl, with accounts and permissions spread across multitudes of SaaS and IaaS apps. Identity compromise is the leading cause of data breaches, with orphaned and over-provisioned accounts posing serious risks to organizations. ConductorOne provides an automation platform for managing the lifecycle of permissions and access across SaaS apps and cloud infrastructure, providing a valuable tool for organizations of all sizes looking to improve their security posture and reduce the risk of unauthorized access. ## Challenge At ConductorOne, a key element of their company philosophy is to meet customers where they are. To roll out an enterprise API to their customers, that meant ensuring maximum accessibility. The first decision they had to make was what protocol they wanted to use to expose their API: REST or gRPC. Internally, their teams built and consumed gRPC services. However, for the public API, they decided to expose a RESTful interface to appeal to the greatest number of users. They built a [custom plugin](https://github.com/ductone/protoc-gen-apigw/tree/main), forked from [gRPC gateway](https://github.com/grpc-ecosystem/grpc-gateway) that generates a Go server with a REST gateway exposed to their users. To ensure maximum accessibility, the ConductorOne product and engineering teams knew they wanted to provide SDKs to lower the barriers for integration. They knew that well-crafted SDKs and documentation equates to happy developers. ConductorOne also wanted a Terraform (TF) provider to allow Terraform code to interact with their API to automate the setup/configuration process and ensure stability of the customer's infrastructure. The team realized that signing up for the creation of SDKs and Terraform providers meant potentially taking on significant development overhead and an ongoing maintenance burden. --- ## Solution The team had prior experience with SDK generators, and recognized that the in-house approach often requires significant rework. In addition, SDK generators don't create documentation for the SDK – so this would have to be built by hand also. With Speakeasy they were able to automatically create a Terraform provider and idiomatic SDKs across multiple languages, with in-repo documentation – all while radically reducing development time. Time is an important dimension, but for ConductorOne, SDK and Terraform provider quality are even more important. As the primary interface for APIs, ConductorOne understood that their SDKs are perhaps the single most important component of their developer experience. With Speakeasy, ConductorOne is able to ensure their SDKs are type safe and idiomatic and that they are able to provide a best-in-class developer experience. --- ## Results Developing 3 SDKs and a Terraform provider, coding by hand, would take a team of developers weeks to produce. By their estimation, utilizing Speakeasy, ConductorOne was able to save between 400 to 500 hours of development for the SDKs, and another 150+ hours for the generation of their Terraform provider.
And the savings continue to compound over time. With Speakeasy, they are able to automatically regenerate their SDKs, provider, and docs each time they update their API. Aside from cost and time savings, one of the greatest ongoing benefits realized by ConductorOne has been eliminating the developer toil and onerous release process involved with updating every SDK whenever the API changes. With Speakeasy, the SDKs and Terraform providers are always up-to-date and versioned. They have peace of mind that every time the API changes, SDKs/TF providers are updated and republished automatically. To see the results first hand, take a closer look at ConductorOne's SDKs and Terraform provider here: - [Go SDK](https://github.com/ConductorOne/conductorone-sdk-go) - [Python SDK](https://github.com/ConductorOne/conductorone-sdk-python) - [Typescript SDK](https://github.com/ConductorOne/conductorone-sdk-typescript) - [TF Provider](https://github.com/ConductorOne/terraform-provider-conductorone) ## Benefits Summary ConductorOne's partnership with Speakeasy has delivered significant advantages: ### Developer Experience By providing high-quality, type-safe, and idiomatic SDKs across multiple languages, ConductorOne has made it easier for developers to integrate with their platform, expanding their potential customer base. ### Time to Market What would have taken weeks of development work was accomplished in a fraction of the time, allowing ConductorOne to focus on their core product rather than SDK maintenance. ### Quality Assurance Speakeasy ensures that SDKs follow best practices for each language, providing a consistent and reliable experience across all supported platforms. ### Continuous Updates The automated pipeline ensures that as ConductorOne's API evolves, their SDKs and Terraform provider stay in sync without additional engineering effort. Gain your own peace of mind, try Speakeasy for yourself for free. [Sign up](https://app.speakeasy.com/) today. # Example of epilot's OpenAPI spec with Speakeasy annotations Source: https://speakeasy.com/customers/epilot import { Card, Table } from "@/mdx/components"; ## Company Info - **Company:** epilot - **Industry:** Enterprise SaaS - **Website:** [epilot.cloud](https://epilot.cloud/) ## Key Takeaways - 8X reduction in expected cost of Terraform Provider ($220K cost saving) - Generated from OpenAPI specs for a minimal learning curve - No need to invest in hiring teams of specialist engineers — maintain focus on core product development --- ## Automating Terraform Provider Creation Cuts Costs by 8X and Accelerates Go-to-Market Prior to Speakeasy, epilot's Solutions Engineering team needed to work with each customer manually to set up their epilot instance. Viljami Kuosmanen, epilot's Head of Engineering, realized that epilot setup could be automated via their existing APIs, and that a language for configuring infrastructure already existed — Terraform. The only piece they were missing was a Terraform provider. Terraform providers allow Terraform code to interact with an API. In epilot's case, they could create Terraform configuration files that communicate with the epilot Terraform provider (and ultimately, the epilot API), to automate the setup/configuration process. This would make setting up a new customer in epilot as easy as running a single command. Viljami recognized that building an entire suite of Terraform providers would require significant creation and maintenance cost however: in fact, **their initial budget estimate was $250K per year to hire a dedicated team of Go engineers.** ## The Solution: OpenAPI-based Terraform Generation with Speakeasy epilot are huge advocates of the OpenAPI framework. Viljami himself is the maintainer of several OpenAPI-based tools such as the OpenAPI backend project. Internally, the epilot team uses their OpenAPI spec as the primary source of truth for their API development, automating as many workflows as possible. All the epilot team had to do was add some simple annotations to their OpenAPI spec, describing the Terraform resources that they wanted to create: ```yaml /v1/permissions/roles/{roleId}: get: operationId: getRole summary: getRole x-speakeasy-entity-operation: Role#get description: Get role by id tags: - Roles parameters: - name: roleId x-speakeasy-match: id in: path required: true schema: $ref: "#/components/schemas/RoleId" responses: "200": description: ok content: application/json: schema: $ref: "#/components/schemas/Role" Role: x-speakeasy-entity: Role oneOf: - $ref: "#/components/schemas/UserRole" - $ref: "#/components/schemas/OrgRole" - $ref: "#/components/schemas/ShareRole" - $ref: "#/components/schemas/PartnerRole" ``` Now, whenever the epilot team updates their OpenAPI specs, Speakeasy's Github workflow automatically regenerates their Terraform providers, runs a suite of tests, and then publishes a new version to the registry. No maintenance work required! ## The Results: New Use Cases And New Customers The proof is in the provider: [epilot's Registered Terraform Providers](https://registry.terraform.io/namespaces/epilot-dev) Through the use of OpenAPI and Speakeasy, epilot was able to automate the creation of Terraform Providers, resulting in a massive acceleration of their roadmap at a fraction of the expected cost. ---
# fivetran Source: https://speakeasy.com/customers/fivetran import { Card } from "@/mdx/components"; [Fivetran](https://fivetran.com) provides automated data movement for analytics, helping enterprises move data from sources to warehouses and lakes. The breadth of Fivetran's user base means enterprise customers constantly need custom connectors for internal APIs and niche third-party tools that fall outside their 700+ pre-built integrations. Building these longtail connectors traditionally requires significant engineering investment—resources most enterprises don't have on hand. For Lead Solution Architect Elijah Davis, the Model Context Protocol (MCP) provided a solution. By exposing [Fivetran's Connector SDK](https://www.fivetran.com/connectors/connector-sdk) and [platform APIs](https://fivetran.com/docs/rest-api) as MCP servers, he hypothesized that it would be possible to enable customers to use AI agents to build custom connectors through natural language. ## Building an enterprise MCP server Elijah started off by building a standalone Python-based MCP server that customers could deploy locally. This worked for proof-of-concept demonstrations, but asking every customer to install a local server wasn't a great user experience. He began looking into building a hosted server, but had some immediate challenges. To run a production server would mean managing infrastructure, configuring an Oauth server, and testing support across various MCP clients. They were staring at weeks of work. Then the team discovered Gram. In 30 minutes Eli was running tests against a hosted server. In a couple of days, he had an OAuth server handling authentication. At the end of the week, he was ready to start serving production traffic. ## Long-tail connector creation with MCP
Fivetran Webinar

Watch Fivetran's MCP-based connector creation in action

The primary workflow addresses one of the most time-consuming tasks in data integration: setting up new connectors. With hundreds of potential connectors and varying requirements across teams, even experienced engineers spend hours on manual configuration. Through the Connector SDK and Gram's MCP tooling, users can describe what they want to connect in natural language. The AI agent generates all necessary files (`connector.py`, `config.json`, `requirements.txt`), incorporates Fivetran best practices, debugs issues when they arise, and deploys directly to Fivetran infrastructure. And sometimes a description isn't even necessary. By providing an API docs URL to the AI agent, it's possible to create a custom connector. The agent learns context from the API documentation, then uses the MCP server to create the connector files, it debugs connection issues, and deploys the solution to Fivetran where it appears immediately in the dashboard. What previously required hours of engineering work is completed in minutes. ## The beginning of a transformation Fivetran successfully deployed MCP integrations to enterprise customers across multiple industries. The solutions team now has a repeatable service offering: work with the customer to understand their workflows, configure custom toolsets in Gram, and enable their teams to automate data operations through natural language. And custom connectors are really just the beginning. Fivetran has embraced AI evolution across their entire product suite. Experiments into AI-based onboarding, and creating chat-based interfaces are only just getting underway. For enterprises exploring AI-powered data operations, Fivetran's approach demonstrates a clear path forward: identify high-value workflows, validate with customers before heavy investment, and leverage specialist infrastructure to focus engineering resources on customer success. In the enterprise world, knowing what not to build yourself is often the fastest path to value. # formance Source: https://speakeasy.com/customers/formance import { Card, Table } from "@/mdx/components"; ## Company Info - **Company:** Formance - **Industry:** FinTech - **Website:** [formance.com](https://www.formance.com/) ## Key Takeaways - Rapid time-to-market, minimal eng burden: SDKs in 2 weeks -- without dedicated engineering resources - Embedded SDKs as part of their OSS Monorepo - High-quality API integrations with SDKs for five languages --- The [Formance](https://www.formance.com/) team is on a mission to build the Open Source Ledger for money-moving platforms. Since coming out of YC W'21, the Paris-based team has scaled its platform to support dozens of fintech companies, like Carbon, Shares, and Cino. These companies trust the Formance stack to provide their critical payments infrastructure. The Formance team relies on Speakeasy to create and maintain SDKs that accelerate their internal development and help scale the reach of their external API, which is critical for product adoption. As a result of working with Speakeasy, the Formance team was able to: - expand to 5+ languages: Typescript, Python, Go, Java, PHP; - start supporting OAuth workflows and BigInt types; - embed their SDKs as part of their OSS Monorepo; - get set up in 2 weeks without dedicated engineering resources. --- ## SDKs Are a Critical Component of API DevEx A core element of delivering that ideal API experience was offering SDKs to their API users, but for a long time, Formance struggled to do this sustainably. Thanks to Speakeasy, automating SDKs via their CI/CD allowed them to gain the advantage of SDKs without sacrificing team time to maintain them. --- ## Impact on Formance's Business
## SDK Strategy Results ### Comprehensive Language Coverage By offering SDKs in TypeScript, Python, Go, Java, and PHP, Formance ensures that their platform is accessible to developers across the entire ecosystem, increasing their market reach. ### Improved Developer Experience Formance's SDKs provide a consistent, high-quality integration experience that aligns with their vision of excellent API DevEx through discoverability, transparency, and elimination of ambiguity. ### Enhanced Core Platform Focus By eliminating the mental load of SDK maintenance, Formance's engineering team can fully focus on building new features and improving the performance of their core platform. ### Seamless OSS Integration The Speakeasy-generated SDKs integrate directly into Formance's open source monorepo, maintaining consistency with their overall development philosophy and workflow. # kong Source: https://speakeasy.com/customers/kong import { YouTube, Card } from "@/mdx/components"; ## Company Info - **Company:** Kong Inc. - **Industry:** B2B SaaS - **Website:** [konghq.com](https://konghq.com) [Kong](https://konghq.com/) is one of the leading API platforms in the industry. Built on the world's most adopted API gateway and supporting over 400B API calls daily, Kong provides the foundation for any company to become an API-first company. ## Increasing Customer Satisfaction with a Terraform Provider Providing a top-quality user experience is imperative for Kong to maintain a steady competitive advantage. Michael Heap, Senior Director of Developer Experience at Kong, ensures that Kong products are as user-friendly as possible, including documentation, automation capabilities, how well the products work together, and more. This led him to explore the Terraform ecosystem. There has always been a demand to configure Kong via Terraform. For a long time, Kong would refer customers to the provider managed by the Kong community. But for large enterprises, a community-maintained provider was a compliance liability that made it a no-go. The bigger issue for the Kong team was the time to market for new features. For customers using the community provider, it would take weeks, sometimes months, before a new feature would be available. That lag between development and availability became increasingly problematic – convincing the Kong team that they needed to offer their enterprise customers a Terraform provider with tier-one support. ## When a Design-First Approach Pays Off In 2022, Kong made a decision to become a design-first company. This means that, before releasing a new product, they have to consider all possible use cases carefully, focus on flexibility and simplicity, and accommodate all levels of users. The company has put significant effort into designing its OpenAPI specs for the best customer experience. This came in handy when they chose the Speakeasy platform to generate the Terraform provider from the OpenAPI spec, resulting in: - the engineering team staying focused on their existing roadmap projects without any detours, - maintained service reliability with no disruption to existing customers, - improvements to Kong's existing OpenAPI specs for other use cases, like documentation, - enterprise customers receiving support when needed, - new feature updates to the Terraform provider as soon as they are released. The provider's automated updates wouldn't be possible if it was maintained solely by in-house resources and without Kong's commitment to a design-first approach. The Speakeasy platform ensures that the provider stays up-to-date with Kong's API by pushing a new branch to the provider's repository whenever an API spec change is detected. This helps Kong ensure that: - their customers always have access to the most recent version of the provider, - their engineering team saves time due to no manual updates required, - the support team's workload is decreased thanks to no discrepancies between the provider and the API, - the company maintains the highest quality level of customer experience. You can read more about Kong's experience working with Speakeasy in [an article they wrote for The New Stack.](https://thenewstack.io/from-zero-to-a-terraform-provider-for-kong-in-120-hours/) Watch Michael Heap talk about the Kong Terraform Provider and his experience with Speakeasy. # mistral Source: https://speakeasy.com/customers/mistral import { Card, CodeWithTabs, Table } from "@/mdx/components"; ## Overview: Offering Always-in-sync SDKs With Minimal Eng Work Mistral AI provides text generation with streaming capabilities, chat completions, embeddings generation, and specialized services like OCR and moderation through their [API](https://docs.mistral.ai/api/). In the fast-paced generative AI landscape, providing millions of developers immediate access to the latest models and features via consistent, reliable SDKs in users' most popular languages is a key competitive differentiator. This case study explains how Mistral AI automated their SDK generation process using Speakeasy to maintain consistent, high-quality client libraries across multiple deployment environments, freeing their team to focus on core AI innovation. ## Technical Context Before automating their SDK generation, Mistral AI's API presented several implementation challenges for SDK development: - Complex API structure: Their completion and chat APIs featured nested JSON with conditional fields and streaming responses, pushing the limits of standard OpenAPI representations. - Multiple authentication schemes: Services running on their own infrastructure as well as on GCP and Azure—each with different authentication requirements and subtle API differences - Rapid feature evolution: New capabilities, like structured outputs, needed to be consistently and quickly available across all client libraries. ## Challenges ### Developer Experience Challenges Before implementing Speakeasy, Mistral AI relied on manually written clients. This manual process struggled to keep pace with rapid API development, leading to several problems for developers using the SDKs: - Feature gap: SDKs often lagged behind the API capabilities, with developers waiting for new features or having to work around missing functionality. - Inconsistent implementations: Features might appear (or behave differently) in one language SDK before others. - Documentation drift: Keeping API documentation, SDK documentation, and SDK implementations synchronized during rapid development cycles was a constant struggle. ### Technical Implementation Challenges The engineering team faced significant technical hurdles maintaining these manual SDKs: - Representing Complex APIs: Accurately representing the complex nested JSON structures, especially for streaming responses, within OpenAPI specifications was difficult. Example structure of a chat completion request: - Multi-Environment Support: Managing the distinct authentication logic and potential subtle API differences across GCP, Azure, and on-premise environments within each SDK was cumbersome. - SDK Consistency: Ensuring feature parity, consistent behavior, and idiomatic usage across both Python and TypeScript implementations required significant manual effort and testing. ## Solution: Automated SDK Generation with Speakeasy Mistral AI adopted Speakeasy's SDK generation platform to automate the process and address these challenges comprehensively. ### Multi-Source Specification Management To handle their different deployment targets and authentication schemes, the Mistral AI team designed a sophisticated workflow leveraging Speakeasy's ability to manage OpenAPI specifications. They used multiple specification sources and applied overlays and transformations to tailor the final specification for each target environment (e.g., adding cloud-specific authentication details or Azure-specific modifications). This approach allowed them to maintain a single source of truth for their core API logic while automatically generating tailored specifications and SDKs for their complex deployment scenarios, replacing tedious manual SDK coding with an automated pipeline. ### Cross-Platform Support Speakeasy enabled Mistral AI to automatically generate and maintain consistent SDKs across their diverse deployment environments, ensuring developers have a reliable experience regardless of how they access the Mistral AI platform:
(Links: [Mistral's Python SDK](https://github.com/mistralai/client-python), [Mistral's TypeScript SDK](https://github.com/mistralai/client-ts)) This automation ensures that whether developers interact with Mistral AI via a managed cloud instance or a self-deployed environment, they benefit from SDKs generated from the same verified OpenAPI source, including necessary configurations (like specific authentication methods) handled during the generation process. The platform provided automated generation for both public-facing SDKs and enhanced internal variants with additional capabilities. ### From Manual to Automated: Collaborative Engineering The transition from manual SDK creation to an automated workflow involved close collaboration between Mistral AI and Speakeasy. This partnership allowed Mistral AI to leverage Speakeasy's expertise and customization capabilities to accurately model their complex API and authentication requirements. **Before Speakeasy:** Based on their earlier client versions, developers had to manually construct request bodies, handle optional parameters explicitly, implement distinct logic for streaming versus non-streaming responses, and manage HTTP requests and error handling directly. This led to more verbose and potentially error-prone code requiring significant maintenance. Union[ChatCompletionResponse, Iterator[ChatCompletionStreamResponse]]: # Manually construct the request data dictionary data = { "model": model, "messages": [message.model_dump() for message in messages], "safe_mode": safe_mode, } # Manually add optional parameters if they are provided if temperature is not None: data["temperature"] = temperature if max_tokens is not None: data["max_tokens"] = max_tokens if top_p is not None: data["top_p"] = top_p if random_seed is not None: data["random_seed"] = random_seed # ... manual checks for other optional parameters # Manual branching logic based on the 'stream' parameter if stream: data["stream"] = True # Manually call internal request method configured for streaming response = self._request( "post", self._resolve_url("/chat/completions"), json=data, stream=True ) # Manually process the streaming response via a separate generator return self._process_chat_stream_response(response) else: # Manually call internal request method for non-streaming response = self._request("post", self._resolve_url("/chat/completions"), json=data) # Manually parse the JSON and instantiate the response object return ChatCompletionResponse.model_validate_json(response.content) # Note: The internal '_request' method (not shown) would contain further manual logic # for handling HTTP calls, authentication headers, and error status codes.`, } ]} /> This manual approach required developers to carefully manage numerous optional fields, different response types depending on parameters like `stream`, and the underlying HTTP interactions for each API endpoint. **After Speakeasy:** The generated code provides clean, idiomatic interfaces with automatic type handling, validation, proper resource management (like context managers in Python), and abstracts away the underlying HTTP complexity. This automated approach enabled Mistral AI to provide a polished, consistent experience for developers, significantly reducing boilerplate and potential integration errors. ## Key Results Mistral AI's implementation of Speakeasy has yielded impressive technical and business outcomes: ### Engineering Efficiency - SDKs automatically update when API changes occur. - Reduced maintenance overhead, freeing up core engineers to focus on AI model development and platform features. - Significant productivity boost for internal SDK consumers e.g. front-end team ### Feature Velocity & Quality - Rapid feature rollout: New API capabilities, like structured outputs, were implemented consistently across SDKs in days, compared to a multi-week timeline previously. - Complete API coverage, ensuring all public endpoints and features are consistently available across supported SDKs. - Improved internal practices: Increased usage of SDKs by internal teams, with Speakeasy's validation helping enforce OpenAPI spec quality and ensuring consistent validation and type-checking across their ecosystem. ## Implementation Journey Mistral AI's journey to fully automated SDK generation followed these key phases: 1. Specification Refinement: Collaborating with Speakeasy to ensure their OpenAPI specifications accurately represented the complex API structure, including streaming and authentication details. 2. Customization & Transformation: Developing necessary transformations (using Speakeasy's customization features) to handle environment-specific logic like authentication. 3. Validation & Testing: Rigorous testing of the generated SDKs across different languages and deployment environments. ## What's Next Mistral AI continues to leverage and expand its Speakeasy implementation: - Automated Test Generation: Implementing Speakeasy's test generation features for comprehensive SDK testing. - CI/CD Integration: Integrating Speakeasy's SDK generation into their existing CI/CD pipeline for fully automated builds and releases upon API updates. - Generated Code Snippets: Adding Speakeasy-generated code examples directly into their API documentation to further improve developer onboarding. - New Model Support: Upcoming models and services, like their advanced OCR capabilities, will utilize Speakeasy-generated SDKs from day one, demonstrating continued confidence in the platform. As Mistral AI expands its offerings with models like Mistral Large, Pixtral, and specialized services, Speakeasy provides the scalable foundation for maintaining a world-class developer experience across their entire API ecosystem. Explore the Speakeasy-generated SDKs and the Mistral AI API documentation: - [Mistral AI Python SDK](https://github.com/mistralai/client-python) - [Mistral AI TypeScript SDK](https://github.com/mistralai/client-ts) - [Mistral AI SDK Documentation](https://docs.mistral.ai/getting-started/clients/) # polar Source: https://speakeasy.com/customers/polar import { Card } from "@/mdx/components"; [Polar](https://polar.sh) provides payment infrastructure designed for software developers. Widely admired for their approach to developer experience and design, the Polar team is always thinking about how to make payments more intuitive, whether that's through a clean API, thoughtful documentation, or a polished web app. When the Model Context Protocol (MCP) was announced, Polar's engineering team faced a familiar challenge: how do you balance building core product with exploring emerging technologies when you're a small team? In February 2025, Polar launched with a locally hosted MCP server, powered by [Speakeasy's generator](/docs/standalone-mcp/build-server). Over the next few months, through observation of their users and their own internal experimentation, they identified three distinct use cases where AI agents connected to their MCP server could have an immediate impact. To start bringing those production use cases to life, they built out their remote MCP server using Gram. ## Use case 1: AI-assisted onboarding Even with a developer-friendly API, initializing a payments platform requires understanding pricing models, configuring products correctly, and navigating multiple screens. For developers new to monetization, this complexity can slow down what should be a quick process. Polar's traditional onboarding flow followed the standard pattern: new customers sign up through the web app, navigate through forms to configure products and pricing, and then integrate the API into their codebase. It's functional, but requires understanding payment terminology and making configuration decisions upfront. With MCP, Polar realized they could flip this experience entirely. Instead of filling out forms, developers could describe their business model in natural language: "I want to charge $10 per month for my API, with usage-based pricing for calls over 1,000." Polar's AI agent would handle creating the appropriate products, setting up pricing tiers, and configuring entitlements automatically. ## Use case 2: Natural language analytics One of the most common reasons users check their payment dashboard is to answer simple questions: "What did I sell most this month?" or "Who was my biggest revenue customer?" These questions typically require navigating analytics dashboards, filtering data, and sometimes writing custom queries. Polar's MCP server will enable a more direct approach. Users can ask questions in natural language, and the AI agent interacts with Polar's MCP to provide answers with derived analytics. ## Use case 3: Operational automation through agents The most ambitious use case involves handling common, but tedious, day-to-day merchant operations through AI agents. Processing refunds, managing subscriptions, and responding to customer issues could all happen through natural language commands rather than manual dashboard interactions. In an ideal world when a complaint comes in, a merchant can just say, 'refund 50% of what this emailer has paid us.' The Polar agent figures out the customer, finds their purchases, and handles the refunding logic. No more manually looking up orders, calculating amounts, and executing refunds through multiple steps. With an MCP-powered agent, the entire workflow collapses into a single natural language instruction. ## Why Gram made sense For a small team, building an MCP server from scratch would mean weeks spent on protocol implementation, authentication flows, and ongoing maintenance – all before delivering any value to customers. Gram's automatic generation from Polar's API meant they could skip the infrastructure work entirely. The team built their AI-assisted onboarding experience in less than a week. The architecture was elegantly simple: [Vercel's AI SDK](https://ai-sdk.dev/docs/introduction) on the frontend for the conversational interface, The Gram-generated MCP server managing tool execution, and Polar's existing API managing the actual configuration. ## What Gram enabled ![List of Polar's tools in the Gram UI](/assets/customers/polar/polar-tools.png) Polar shipped their first AI-powered use case to production in under a week, validating the concept before committing significant engineering resources. As their APIs evolve, Gram automatically updates the MCP server without manual intervention, eliminating what would otherwise be an ongoing maintenance burden. While automatic generation from their OpenAPI spec covers Polar's entire API surface area, Gram's custom tool builder provides flexibility when needed. For complex workflows like refund processing that require specialized business logic, Polar can create hand-crafted tools that encapsulate the full operation into a simple, natural language command. ## Looking toward an AI-integrated future The partnership with Speakeasy has allowed Polar to pioneer AI-powered payment workflows without diverting engineering resources from their core product. As they expand the analytics and operational automation use cases based on real usage patterns, the decision to build MCP with Gram continues to pay dividends. The Polar team is particularly excited about Gram's upcoming observability features. Once available, they'll be able to see which tools get called and where users experience friction, providing data-driven insights into which use cases resonate most. This will help them prioritize future investment: double down on the onboarding flow, or expand the analytics and operational automation capabilities? For developer tools companies exploring AI integration, Polar's approach offers a clear roadmap: identify use cases through exploration, leverage specialist infrastructure to ship quickly, and invest engineering resources where they create the most differentiated value. Sometimes the fastest path to innovation is knowing what not to build yourself. # prove Source: https://speakeasy.com/customers/prove import { Card, Callout, CodeWithTabs, Table } from "@/mdx/components"; ## Company Info - **Company:** Prove - **Industry:** Identity Verification - **Website:** [prove.com](https://www.prove.com) [Prove](https://www.prove.com) is a leading provider of identity verification solutions, delivering services like Prove Pre-Fill®, Prove Identity℠, and Prove Verified Users℠ through robust APIs. ## Overview: Automating SDK Generation to Speed Up API Integration Prove adopted Speakeasy to automate the generation and maintenance of high-quality, server-side SDKs. This has resulted in significantly reduced integration times, improved developer experience, and faster time-to-market for Prove and its customers. ## Challenge: API Docs Alone Leads to Slower, Error-Prone Integrations Prove's customers and their own development team were spending too much time hand-building API integrations from API docs: - **Internal inefficiencies:** Engineers had to write boilerplate code for service-to-service integrations. This led to wasted time and created opportunities for inconsistencies between implementations. - **Customer onboarding delays:** Without SDKs, customers were left to build everything from scratch: handling OAuth token management, request/response formats, retries, and parsing API docs. With 10–12 endpoints per solution, implementation timelines stretched into weeks. --- ## Solution: Automated SDKs Reduces Manual Work, Accelerates Integrations Prove chose Speakeasy to automate the generation of server-side SDKs for both internal and external use. Speakeasy allows Prove to:
### Language Support and Distribution Prove leverages Speakeasy to generate and maintain several SDKs: ``` Go → https://github.com/prove-identity/prove-sdk-server-go TypeScript → https://github.com/prove-identity/prove-sdk-server-typescript Java → https://github.com/prove-identity/prove-sdk-server-java ``` These SDKs handle complex authentication flows, error handling, request formatting, and response parsing, making it substantially easier for developers to integrate with Prove's identity verification services. ## Results: Faster Integrations and Enhanced Developer Experience Since adopting Speakeasy, Prove has experienced significant improvements in both internal development and customer onboarding: ### Reduced Integration Time
### Key Improvements - **Reduced Internal Integration Time:** Internal teams can now integrate with Prove's services in a matter of hours instead of days. - **Faster customer onboarding:** Thanks to simplified APIs and the availability of SDKs, Prove has cut customer implementation time in half, from initial conversation to production deployment. - **Improved Developer Experience:** Both internal and external developers benefit from well-documented, easy-to-use SDKs, reducing frustration and accelerating development cycles. The SDKs handle OAuth token management and simplify request/response handling, allowing developers to focus on their application logic. - **Enhanced API Consistency:** Speakeasy ensures that SDKs are always in sync with the latest API specifications, eliminating API drift and reducing integration errors. ### Key Takeaways - Automated SDK generation significantly reduces integration time for both internal and external developers - Consistent SDKs across multiple languages ensure reliability and reduce maintenance overhead - Handling authentication, request formatting, and response parsing in SDKs simplifies developer experience - Continuous generation from OpenAPI specs eliminates API drift and ensures documentation accuracy Discover how Speakeasy can help your organization streamline API integration and improve developer experience. # solarwinds Source: https://speakeasy.com/customers/solarwinds import { Card, Table } from "@/mdx/components"; ## Company Info - **Company:** SolarWinds - **Industry:** IT Management - **Website:** [solarwinds.com](https://www.solarwinds.com/) ## Key Takeaways - Rapid API Integration: Reduced development cycles from weeks to days - Automated Testing: Ensures API updates are automatically validated - Intuitive SDKs: Provides a superior developer experience with easy-to-integrate solutions --- ## About SolarWinds [SolarWinds](https://www.solarwinds.com/) delivers seamless resiliency and efficiency, safeguarding across hybrid IT environments by integrating observability, database performance, and IT service management. SolarWinds has collaborated with Speakeasy to generate robust Software Development Kits (SDKs), significantly reducing API integration time and transforming the developer experience, leading to faster and more accessible innovation. ## The Challenge: Complexity, Time, and Maintenance SolarWinds has seen usage of their API grow significantly over time – not just from customers, but also from internal teams who have developed their own tools atop the APIs. Given this increased interest in the REST API, SolarWinds recognized that improving the API developer experience was a key priority. Existing manually-created SDK clients were effective, but had become complex and time-consuming to maintain. * **Evaluating open-source options:** The team considered Open-Source tools like the OpenAPI Generator (formerly Swagger Codegen) but determined that the required staffing and maintenance efforts were unsustainable for an enterprise-scale solution. * **Developer friction:** Without a reliable, automated SDK solution, API updates would require extensive rework, increasing the risk of errors and frustrating developers. ## The Solution: Speakeasy After evaluating multiple commercial options, SolarWinds selected Speakeasy for its: * **Ergonomic SDKs:** Speakeasy-generated SDKs offered intuitive interfaces and ease of integration, allowing developers to start building immediately. * **Automated testing and documentation:** Built-in test generation and seamless documentation integration ensured that every API change was automatically validated and reflected in up-to-date code samples. ## The Results: Enhanced Engineering Efficiency and Developer Experience SolarWinds is expanding its SDK offerings to six languages, with the SolarWinds SDK for Go already available. This multi-language support empowers more developers to build on the SolarWinds® Platform.
## Ready to Accelerate Your API Integration? The SolarWinds journey—from in-house struggles to a streamlined, automated SDK solution with Speakeasy—shows how the right tools can revolutionize developer experience and contribute to a more efficient and resilient IT environment. By reducing integration times and eliminating maintenance burdens, SolarWinds can focus on innovation. [Book a demo](https://www.speakeasy.com/book-demo) or [start a free Speakeasy trial](https://app.speakeasy.com/) today. --- ## Implementation Details SolarWinds' implementation of Speakeasy has delivered significant technical benefits: ### Multi-Language Support Expanding SDK offerings to six programming languages, beginning with Go and systematically adding support for additional languages to meet diverse developer needs. ### Automated SDK Generation Transitioning from manually-created SDKs to an automated pipeline that generates SDKs directly from OpenAPI specifications, ensuring consistency across all languages. ### Integrated Testing Framework Leveraging Speakeasy's built-in testing capabilities to automatically validate API changes, reducing the risk of integration issues and regression bugs. ### Developer-First Design Focusing on creating intuitive, ergonomic SDKs that prioritize developer experience and reduce the learning curve for API integration. # unified Source: https://speakeasy.com/customers/unified import { Card } from "@/mdx/components"; Unified API platform for B2B SaaS: 100+ integrations spanning HR, ATS, CRM, and Authentication in just hours. ## Company Info - **Company:** Unified.to - **Industry:** unified API - **Website:** [unified.to](https://unified.to) ## Unified.to Prioritized the Usability of Their API [Unified.to](https://www.unified.to/) is building “one API to integrate them all” — enabling developers to rapidly integrate with third-party systems across HR, ATS, CRM, and more, all through a single API. Unified.to knew that in order for their API to succeed, they had to offer high-quality developer experience. The most surefire way to achieve this is through SDKs. Unified.to's users simply download the SDK in their favorite programming language and call the relevant method. Dev-friendly features like in-IDE code completion and typesafety are built-in, and the developer doesn't need to spend any time with low-ROI boilerplate integration code. Unified.to quickly realized that creating all the SDKs themselves would be prohibitively expensive. They considered open-source tooling but felt that the quality of the output didn't provide the leading developer experience — and would also still require a lot of work to automatically keep the SDKs up-to-date.
## Speakeasy Creates and Maintains Multiple SDKs For Unified.to Through Speakeasy, Unified.to was able to offer SDKs in six different languages — in just one week, and without needing to hire additional engineering resources. Due to Speakeasy's automated SDK maintenance, customers always have access to the latest SDK version as soon as the API changes. This ensures customers always have the latest and greatest, while saving tens of hours of engineer time per month on SDK maintenance. Unified.to was also impressed with the responsiveness of the Speakeasy team: [Unified.to](https://www.unified.to/) already provides a great Unified API experience to developers. Now, thanks to idiomatic SDKs in six languages from Speakeasy, Unified.to's API users get an even more intuitive, friction-free integration experience. # unstructured Source: https://speakeasy.com/customers/unstructured import { Card, Table, YouTube } from "@/mdx/components"; ## Company Info - **Company:** Unstructured - **Industry:** AI/Data Processing - **Website:** [unstructured.io](https://unstructured.io/) ## How Unstructured Makes Its Enterprise API More Accessible With Speakeasy [Unstructured](https://unstructured.io/) enables companies to talk to their data. They extract unstructured data from any file format and transform it into AI-friendly JSON files that can be passed to LLMs. They're currently the only company doing this kind of AI pre-processing – and have seen tremendous success. In fact, since the summer of 2022, Unstructured's OSS library has been downloaded 6.4M times and used by 27,000 organizations. When the Unstructured team was working on building their enterprise API, they wanted to provide a seamless, high-class experience to their enterprise customers every step of the way. They understood the massive difference that SDKs can make for streamlining and elevating the API integration experience and started investigating how to offer them to their customers. ## Choosing an SDK Partner They decided against building SDKs in-house, as it would create a massive burden for the team and distract focus from other core development priorities. By creating and maintaining their SDKs in the Speakeasy platform, the Unstructured dev team was able to save time and cognitive overhead. This has proven to be the right decision considering the rapid growth Unstructured has been enjoying. Additionally, Unstructured wanted their SDK partner to be highly experienced with OpenAPI and advise them on spec management for a public API. They chose Speakeasy because it ticked all these boxes and provided the expertise they were looking for. --- ## API Experience Plan While planning their top-quality API experience, the Unstructured team wanted to focus on a few key points:
With Speakeasy, Unstructured is able to maintain Typescript and Python SDKs that offer an amazing integration experience to their users. These SDKs are now the primary way developers are directed to integrate with Unstructured. Great API accessibility is paramount to Unstructured's product usage, and having a partner like Speakeasy that they can fully rely on has enabled the team to continue developing their product while knowing that their users are equipped with all the necessary tools. --- ## Key Benefits for Unstructured ### Focus on Core Product By outsourcing SDK generation to Speakeasy, Unstructured's engineering team can focus on enhancing their core data processing capabilities rather than SDK maintenance. ### Consistent Developer Experience Speakeasy ensures that Unstructured's SDKs follow best practices for each language, providing a consistent and reliable experience that makes their API more accessible. ### Automatically Updated Documentation As Unstructured's API evolves, their documentation stays in sync without additional engineering effort, ensuring developers always have current information. ### Enterprise-Ready Features Critical enterprise features like error handling, retries, and authentication are automatically included in the SDKs, making Unstructured's paid API offering more attractive to large customers. ### Reduced Support Burden Well-designed SDKs with informative error messages help users troubleshoot issues themselves, reducing the support load on Unstructured's team as they scale. # Getting Started with Docs MD Source: https://speakeasy.com/docs/docs-md/getting-started import { Callout } from "@/mdx/components"; ## Installation Install docs from npm with: ```bash npm install @speakeasy-api/docs-md ``` If you use pnpm, you'll need to either run `pnpm add jotai motion` or `pnpm i --shamefully-hoist` to pick up some transient dependencies currently needed at the root of `node_modules` This step is part of a temporary mechanism for getting sidebar styling to inherit properly, and will be removed before the public beta in July. ## Configuration The first step in configuration is to create a file named `speakeasy.config.mjs`. This file can live anywhere, but convention is to put it beside other configuration files (e.g. `package.json`, `eslint.config.js`, etc.). Start by specifying the path to your OpenAPI spec file: ```ts export default { spec: "./my-openapi-spec.yaml", }; ``` The next step in configuration configuration of docs looks a little different for [Docusaurus](#docusaurus) vs [Nextra](#nextra). See the relevant section below for instructions. Once core configuration is complete, check out [Try It Now](#try-it-now) for instructions on configuring live-querying of your backend from the docs. ### Docusaurus For Docusaurus, you'll want to add this information to your speakeasy config file: ```ts export default { spec: "./my-openapi-spec.yaml", output: { framework: "docusaurus", pageOutDir: "./docs/api", componentOutDir: "./src/components/speakeasy", }, }; ``` Let's go over what these properties do: - `framework` tells docs to run some Docusaurus specific compilation, such as configuring `_category_.json` files for pages - `pageOutDir` sets the directory where we'll put `.mdx` files that represents a page - Assuming you are using the classic preset in `docusaurus.config.js` and have configured the docs option, then you want to pick somewhere in the `docs/` folder - We automatically configure `_category_.json` files for generated docs, so that these pages properly show up in the left sidebar nav - `componentOutDir` set the directory where we'll put supporting code - This code does not represent a page, and so should not go in the `docs/` folder ### Nextra For Nextra, you'll want to add this information to your speakeasy config file ```ts export default { spec: "./my-openapi-spec.yaml", output: { framework: "nextra", pageOutDir: "./src/app/api", componentOutDir: "./src/components/speakeasy", }, }; ``` Let's go over what these properties do: - `framework` tells docs to run some Nextra specific compilation, such as configuring `_meta.ts` files for pages - `pageOutDir` sets the directory where we'll put `.mdx` files that represents a page - Assuming you are using the [documentation theme](https://nextra.site/docs/docs-theme/start), then you'll want to pick somewhere in `src/app`. - IMPORTANT: make sure _not_ to have a [route group](https://nextjs.org/docs/app/api-reference/file-conventions/route-groups) in the path to generated docs. Experimentation has shown that this breaks the left sidebar - We automatically configure `_meta.ts` files for generated docs, so that these pages properly show up in the left sidebar nav - `componentOutDir` set the directory where we'll put supporting code - This code does not represent a page, and so should not go in the `src/app` folder ### Common Display options You can tweak how docs are displayed with the following properties: - `showSchemasInNav`: whether or not to generate links to schemas in the left sidebar nav. Default `true` - `maxTypeSignatureLineLength`: controls when inline type signatures wrap into multiple lines. Default `80` - If you notice weird wrapping issues with inline type signatures, try increasing or decreasing this value. Otherwise, we recommend not setting this value - `maxSchemaNesting`: controls how deeply to nest schemas before breaking deeper schemas into the right popout ## Try It Now If you would like to use the Try It Now feature, which allows users to live-query your backend using TypeScript code samples, you'll first need your SDK published to npm. We will add support for Try It Now without needing the SDK published to npm before docs reaches General Availability in August. To configure Try It Now, add the following to your `speakeasy.config.mjs` file: ```ts export default { ... tryItNow: { npmPackageName: 'my-awesome-npm-package', sdkClassName: 'MyAwesomeClassName' } }; ``` Let's break down what these two properties do: - `npmPackageName` is the name of the package containing a published version of your SDK - This name is what you would use with `npm install my-awesome-npm-package` - `sdkClassName` is the name of the main export from the npm package - This name is what you would use with `import { MyAwesomeClassName } from 'my-awesome-npm-package'` The current implementation of Try It Now is very new and has some rough edges. We'll work to polish this feature considerably before General Availability in August. When enabling Try It Now, the spec used to build the npm package and the spec pointed to by `spec` in the config file *must* match, including overlays. Otherwise, code samples most likely will not run correctly due to naming mismatches. ## Building To build docs pages, add this to the `scripts` section of your `package.json`: ```json "build-api-docs": "docs-md", ``` Whenever your spec changes, run `npm run build-api-docs` to update files in `pageOutDir` and `componentOutDir`. This command supports a few flags: - `-C` / `--clean`: delete the contents of page and component out directories - This flag is useful for removing pages that no longer exist, e.g. because you renamed an operation tag - `-c` / `--config`: specify the configuration file to use - By default, we look for the config file in directory that the command is run from, e.g. the same folder that `package.json` is in when building docs with npm scripts Once docs files have been built, you can use the standard Docusaurus or Nextra scripts for previewing or building the site, e.g. `npm run dev` or `npm run build`. # Using Zapier with Gram-hosted MCP servers Source: https://speakeasy.com/docs/gram/clients/using-zapier-with-gram-mcp-server [Zapier](https://zapier.com/) is a leading automation platform that connects thousands of apps. With the **MCP Client by Zapier** integration, you can connect Zapier to your [Gram-hosted MCP servers](/blog/release-gram-beta), allowing you to trigger tools and actions on your MCP server directly from your Zapier workflows. This guide shows you how to connect Zapier to a Gram-hosted MCP server using an example Push Advisor API. You'll learn how to set up the connection, create a Zap that you can interact with in a Slack channel and that uses custom MCP tools to check whether today is a good day to push to production. ## Prerequisites To follow this tutorial, you need: - Admin access to a [Slack](https://slack.com/) workspace - A [Gram account](/product/gram) - A [Zapier account](https://zapier.com/) (7-day free trial available) ## Creating a Gram MCP server If you already have a Gram MCP server configured (like the Push Advisor from other guides), you can skip to [connecting Zapier to your Gram-hosted MCP server](#creating-a-zap-with-gram-mcp). ### Setting up a Gram project In the [Gram dashboard](https://app.getgram.ai), click **New Project** to create a new project. Enter a project name and click **Submit**. ![get-started](/assets/docs/gram/img/guides/zapier/get-started.png) Once you've created the project, click the **Get Started** button. Choose **Start from API**. Gram then guides you through the following steps. ### Step 1: Upload the OpenAPI document Upload the [Push Advisor OpenAPI document](https://github.com/ritza-co/gram-examples/blob/main/push-advisor-api/openapi.yaml), enter `Push Advisor` as the API name, and click **Continue**. ![open-api-doc](/assets/docs/gram/img/guides/zapier/upload-openAPI.png) ### Step 2: Create a toolset Give your toolset a name (for example, `push_advisor`) and click **Continue**. ![name-toolset](/assets/docs/gram/img/guides/zapier/name-toolset.png) Notice that the **Name Your Toolset** dialog displays the names of the tools that Gram will generate from your OpenAPI document. ### Step 3: Configure MCP Enter a URL slug for the MCP server (for example, `zapier-push-advisor`) and click **Continue**. ![mcp-slug](/assets/docs/gram/img/guides/zapier/mcp-slug.png) This finalizes the creation of the toolset from the OpenAPI document. ### Step 4: Test the toolset We can test that our tools work correctly using the **Gram Playground**. Navigate to the **Playground** page. The chatbot page lists all our available tools on the left. Ask some questions in the chat that would trigger tool calls. For example: ``` Can I push to prod? ``` The AI determines the correct tool to call and returns a response based on the answer data. ![playground](/assets/docs/gram/img/guides/zapier/playground.gif) ### Creating an API key Navigate to **Settings** and click **New API Key**. Name your key and click **Create**. Then copy the key and store it somewhere secure (we'll use it later to integrate with Zapier). ![gram-api-key](/assets/docs/gram/img/guides/zapier/gram-api-key.png) ### Publishing an MCP server Go to the **MCP** tab and click on your toolset. Click **Enable**, then scroll down and change the visibility to **Public**. ![public-visibility](/assets/docs/gram/img/guides/zapier/public-visibility.png) Finally, copy the **Hosted URL** from the configuration (for example, `https://app.getgram.ai/mcp/your-mcp-slug`). We'll use the URL along with our API key to integrate with Zapier. ## Creating a Zap with Gram MCP Let's create a Zap that interacts with our user through Slack and makes use of our MCP toolset. To get started, sign in to your Zapier account, click **+ Create**, and choose **Zaps**. ![zapier-create](/assets/docs/gram/img/guides/zapier/zapier-create.png) ### Step 1: Create the Trigger Click the placeholder for the trigger to open the trigger configuration screen and select **Slack** from the options list. For the **Trigger event** select **New Message Posted to Channel**, then sign in to your Slack account under **Account** and press **Continue**. ![trigger-config-1](/assets/docs/gram/img/guides/zapier/trigger-config-1.png) Next, in the **Configure** tab, select your preferred **Channel** from your Slack workspace and click **Continue**. ![trigger-config-2](/assets/docs/gram/img/guides/zapier/trigger-config-2.png) Before testing the trigger, open your chosen Slack channel and send a message that we can use for the remaining setup (for example, *"Should I push?"*). Then test the trigger, select that message from the recent records list, and click **Continue with selected record**. ![trigger-config-3](/assets/docs/gram/img/guides/zapier/trigger-config-3.png) ### Step 2: Add the MCP Client action From the Actions menu, search for `MCP` and choose **MCP Client**. Select **Run Tool** as the **Action event**. ![mcp-client](/assets/docs/gram/img/guides/zapier/mcp-client.png) Click **Sign in** under **Account** and enter your Gram **Server URL** and **Bearer Token** (this is the API key you copied from Gram earlier). Set **Transport** to **Streamable HTTP** and set **OAuth** to **No**, then click **Yes, Continue to MCP Client**. ![zapier-gram-account](/assets/docs/gram/img/guides/zapier/zapier-gram-account.png) Click **Continue**. In the **Configure** section, click the **Tool** dropdown to load the list of tools from your Gram server. Select the `can_i_push_to_prod` tool. ![select-tool](/assets/docs/gram/img/guides/zapier/select-tool.png) Click **Continue** and then **Test step**. You should see a successful response from the Gram MCP server containing the tool result. ![tool-result](/assets/docs/gram/img/guides/zapier/tool-result.png) Once you're confident that you've connected the Gram server, click the **+** button to add another action. ### Step 3: AI response To demonstrate how we can integrate an LLM into our workflow using Zapier, we will add an **AI by Zapier** action. We will pass it the result of our MCP tool call and get a customized response. When we add the action, we are presented with a prompt builder screen. Here we can customize our AI Model to do exactly what we want. Select **Custom prompt** for **Build mode** and select your preferred **Model**. Under **Input fields**, press **+ Add field**. Give the field a name, then under **Field value**, press the **+** sign, and find the **Structured Content Reason** field returned from the MCP tool. Finally, set the **Content Type** to **Text** and press **Save**. ![input-ai-field](/assets/docs/gram/img/guides/zapier/input-ai-field.png) In the **Prompt** field, instruct the bot to provide a friendly response to questions about pushing to production, making sure to use the result of the tool. A stronger prompt will cause the AI to behave more predictably. For example, this is a reasonably strong prompt: ``` You are a helpful and friendly DevOps assistant for a software engineering team. A user has just asked if it is safe to push to production. You will be provided with the raw output from the "Push Advisor" tool (should_i_push), which checks if we are allowed to push to production. Your task is to write a concise Slack reply based on this status: 1. If the status is POSITIVE (e.g., "yes", "go for it", "safe", "chill"): - Give them the green light. - Be encouraging and enthusiastic e.g., "All systems go! 🚀" or similar. 2. If the status is NEGATIVE (e.g., "no", "hold", "unsafe", "bad vibes"): - Warn them strictly not to push. - Be polite but firm (e.g., "Hold your horses! 🛑 The vibes are off."). 3. If the status is unclear: - Advise caution. **Rules:** - Keep the message short (under 2 sentences). - Do not mention "JSON" or internal data structures. - Use an appropriate emoji. - Address the team directly. ``` Zapier will give you an estimation of your prompt's strength and allow you to generate a sample preview response, so you can see whether you're satisfied with its behavior. Once you're happy with the prompt, click **Finish**. ![prompt-config](/assets/docs/gram/img/guides/zapier/prompt-config.png) ### Step 4: Reply in Slack Add a final action step and select **Slack**. Choose **Send Channel Message** as the **Action event** and click **Continue**. ![slack-send](/assets/docs/gram/img/guides/zapier/slack-send.png) In the **Channel** field, select the same channel as the trigger. In the **Message Text** field, insert the output from the **AI by Zapier** step. Make sure **Send as a bot?** is set to **Yes**. This ensures that the bot message doesn't kick off the trigger action, causing a loop. ![slack-message-config](/assets/docs/gram/img/guides/zapier/slack-message-config.png) Click **Continue** and test the step to send a message to Slack. ### Step 5: Deploy and test your Zap To deploy and enable your Zap, first rename it something relevant, then click **Publish**. ![rename-and-deploy](/assets/docs/gram/img/guides/zapier/rename-and-deploy.png) Your Zap is now live, and you can test it by sending a message in your Slack channel. ![slack-message](/assets/docs/gram/img/guides/zapier/slack-message.png) ## What's next You have successfully connected Zapier to a Gram-hosted MCP server. This allows you to bring any API or tool hosted on Gram into your Zapier automation workflows. When you add more tools to your Gram toolset, they become available in the **Run Tool** dropdown in Zapier. Ready to build your own MCP server? [Try Gram today](/product/gram) and see how easy it is to turn any API into agent-ready tools. # docs Source: https://speakeasy.com/docs import { Button } from "@/components/ui/button"; import { CardGrid } from "@/mdx/components"; import { cards, intro } from "@/lib/data/docs/overview"; import { TechCards } from "@/components/card/variants/docs/tech-cards"; import { DocsHeaderIndex } from "@/components/nextra/docs/docs-header-index";

Idiomatic SDKs

# Core concepts Source: https://speakeasy.com/docs/sdks/core-concepts The core concepts explained on this page are essential to understanding Speakeasy SDKs. To skip to getting started with the platform, [go here](/docs/sdks/create-client-sdks). ## Generation workflows A workflow is how the Speakeasy platform defines the process of generating a [target](#Target) from a [source](#Source). A workflow is defined in a `workflow.yaml` file stored in the root of the target repository in the `.speakeasy` directory. A workflow is run using the `speakeasy run` command. Workflows can be run locally for fast iteration, or via a set of GitHub Actions for production usage. For a complete reference of the GitHub workflow, see [the documentation](/docs/manage/github-setup).
![Workflow diagram](/assets/docs/core-concepts.png)
### Sources A source is one or more OpenAPI documents and OpenAPI overlays merged to create a single OpenAPI document. - **OpenAPI specification (OAS)** is a widely accepted REST specification for building APIs. An OpenAPI document is a JSON or YAML file that defines the structure of an API. The Speakeasy platform uses OpenAPI documents as the source for generating SDKs and other code. - **OpenAPI overlay** is a JSON or YAML file used to specify additions, removals, or modifications to an OpenAPI document. Overlays enable users to alter an OpenAPI document without making changes to the original document. ### Targets A target refers to an SDK, agent tool, docs, or other code generated from sources. - **[SDKs](/docs/sdks/create-client-sdks)** are available in 8 languages (and growing). Language experts developed each SDK generator to ensure a high level of idiomatic code generation. For the full details on the design and implementation of generation for each language, see the [SDK design documentation](/docs/languages/philosophy). - **[Agent tools](/docs/standalone-mcp/build-server)** are a new surface for interacting with APIs. They provide a way for LLMs and other agentic platforms to interact with APIs. We support [MCP server generation](/docs/standalone-mcp/build-server) with other tools on the way. - **[Documentation](/docs/sdk-docs/code-samples/generate-code-samples)** is available in the form of an API reference. Generated docs will include with SDK code snippets for every API method. Code snippets can also be embedded into an existing documentation site. - **[Terraform providers](/docs/create-terraform)** can be generated from an annotated OpenAPI document. Terraform providers do not map 1:1 with APIs and so annotations are required to specify the Terraform entities and their methods. ### Workflow file syntax The `workflow.yaml` workflow file is a YAML file that defines the steps of a workflow. The file is broken down into the following sections: ```yaml workflowVersion: 1.0.0 speakeasyVersion: latest sources: my-source: inputs: - location: ./openapi.yaml - location: ./overlay.yaml - location: ./another-openapi.yaml - location: ./another-overlay.yaml # .... more openapi documents and overlays can be added here # more inputs can be added here through `speakeasy configure sources` command # .... # .... targets: python-sdk: target: python source: my-source # more inputs can be added here through `speakeasy configure targets` command # .... # .... ``` The workflow file syntax allows for 1:1, 1:N, or N:N mapping of `sources` to `targets`. A common use case for 1:N mapping is setting up a monorepo of SDKs. See our [monorepo guide](/guides/sdks/creating-a-monorepo) for details. ### Workflow steps #### Validation Validation is the process of checking whether an OpenAPI document is ready for code generation. The Speakeasy platform defines the default validation rules used to validate an OpenAPI document. Validation is done using the `speakeasy validate` command, and validation rules are defined in the `lint.yaml` file. By default the `validate` CLI command will use the `speakeasy-default` linting ruleset if custom rules are not defined. #### Linting Linting is the process of checking an OpenAPI document for errors and style issues. The Speakeasy platform defines the default linting rules used to validate an OpenAPI document. Linting is done using the `speakeasy lint` command, and linting rules are defined in the `lint.yaml` file. #### Testing Testing is the process of checking a generated target for errors. The Speakeasy platform generates a test suite for each target, which can be run using the `speakeasy test` command. A test will be created for each operation in the API. #### Release and versioning Speakeasy automatically creates releases and versions for your target artifacts. The release and version are defined in the `gen.yaml` file and used to track the state of a generation and create a release on the target repository. [Releases](https://docs.github.com/en/repositories/releasing-projects-on-github/about-releases) are used synonymously with GitHub releases, the primary way Speakeasy distributes artifacts. For more information on how to manage versioning, see our [versioning reference](/docs/manage/versioning). #### Publishing Publishing is the process of making a generated target available to the public. The Speakeasy platform generates a package for each target, which can be pushed to the relevant package manager. # Generate SDKs from OpenAPI Source: https://speakeasy.com/docs/sdks/create-client-sdks import { Button, Callout, CardGrid, CodeWithTabs, Screenshot, Table, } from "@/mdx/components"; import { supportedLanguages } from "@/lib/data/docs/languages"; This page documents using the Speakeasy CLI to generate SDKs from OpenAPI / Swagger specs. For a more UI-based experience, use the Speakeasy app:
## Install the Speakeasy CLI After signing up, install the Speakeasy CLI using one of the following methods: For manual installation, download the latest release from the [releases page](https://github.com/speakeasy-api/speakeasy/releases), extract the binary, and add it to the system path. --- ## Speakeasy Quickstart For first-time SDK generation, run `speakeasy quickstart`. ```bash speakeasy quickstart ``` ### Authentication & account creation The CLI will prompt for authentication with a Speakeasy account. A browser window will open to select a workspace to associate with the CLI. Workspaces can be changed later if required. If you've not previously signed up for an account, you will be prompted to create one. New accounts start with a 14 day free trial of Speakeasy's business tier, to enable users to try out every SDK generation feature. At the end of the trial, accounts will revert to the free tier. No credit card is required. Free accounts can continue to generate one SDK with up to 50 API methods free of charge. Please refer to the pricing page for update information on each [pricing tier](https://www.speakeasy.com/pricing). ### Upload an OpenAPI document After authentication, the system prompts for an OpenAPI document: Provide either a link to a remote hosted OpenAPI document, or a relative path to a local file in one of the supported formats:
If the spec is in an unsupported format, use one of the following tools to convert it: - [Swagger 2.0 -> OpenAPI 3.0](https://editor.swagger.io/): go to **Edit > Convert to OpenAPI 3.0** - [Postman -> OpenAPI 3.0](https://kevinswiber.github.io/postman2openapi/) ### Select artifact type After configuring the OpenAPI document, the next step prompt is to choose the type of artifact being generated: SDK or MCP. Select SDK, and a prompt will appear to choose the target language: Choosing Terraform will default you back to the CLI native onboarding. [Editor support for Terraform previews](https://roadmap.speakeasy.com/roadmap?id=a8164ebf-55e1-4376-b42e-4e040c085586) coming soon. For each language, Speakeasy has crafted generators with language experts to be highly idiomatic. Follow the links below for all the details on the design decisions that have gone into each language we support: ### Complete the SDK configuration Speakeasy validates the specifications and generates the SDK after receiving all inputs. The process executes [`speakeasy run`](/docs/speakeasy-reference/cli/run) to validate, generate, compile, and set up the SDK. A confirmation message displays the generated SDK details upon successful completion: ## Iterating on the SDK with Studio If the SDK is successfully generated, there will be a prompt asking the user to open the SDK studio. The Studio is a web GUI that helps users make look & feel improvements to their SDKs. It uses [OpenAPI Overlays](/openapi/overlays) to preserve the original OpenAPI specification while allowing users to make changes to the generated SDK. Saved changes in the Studio automatically triggers a regeneration of the SDK locally. It is also possible to make changes without the Studio. Check out the following guide on [customizing SDKs](/docs/customize-sdks/) for all the details. ## Next Step: Uploading the SDK to GitHub Once the SDK is ready, upload it to GitHub by following the [Github setup guide](/docs/manage/github-setup) # Customize security and authentication Source: https://speakeasy.com/docs/sdks/customize/authentication/configuration import { Callout, CodeWithTabs } from "@/mdx/components"; ## Scope authentication ### Global security Global security allows users to configure the SDK once and then reuse the security configuration for all subsequent calls. To use global security, define the security configuration in the `security` block at the root of the SDK: ```yaml paths: /drinks: get: operationId: listDrinks summary: Get a list of drinks. description: Get a list of drinks, if authenticated this will include stock levels and product codes otherwise it will only include public information. tags: - drinks components: securitySchemes: api_key: type: apiKey name: api_key in: header security: # Here - api_key: [] ``` In the resulting SDK, the user can define the security configuration in the SDK's instantiation. It will then be automatically applied to all subsequent method calls without needing to be passed in as an argument: ", }); const result = await sdk.drinks.listDrinks(); // Handle the result console.log(result); } run();`, }, { label: "Python", language: "python", code: `import sdk s = sdk.SDK( api_key="", ) res = s.drinks.list_drinks() if res.drinks is not None: # handle response pass`, }, { label: "Go", language: "go", code: `package main import ( "context" "log" "speakeasy" "speakeasy/models/components" ) func main() { s := speakeasy.New( speakeasy.WithSecurity(""), ) ctx := context.Background() res, err := s.Drinks.ListDrinks(ctx) if err != nil { log.Fatal(err) } if res.Drinks != nil { // handle response } }`, }, { label: "Java", language: "java", code: `package hello.world; import dev.speakeasyapi.speakeasy.SDK; import dev.speakeasyapi.speakeasy.models.components.*; import dev.speakeasyapi.speakeasy.models.components.Security; import dev.speakeasyapi.speakeasy.models.operations.*; import dev.speakeasyapi.speakeasy.models.operations.ListDrinksResponse; import java.math.BigDecimal; import java.math.BigInteger; import java.time.LocalDate; import java.time.OffsetDateTime; import java.util.Optional; import static java.util.Map.entry; public class Application { public static void main(String[] args) { try { SDK sdk = SDK.builder() .apiKey("") .build(); ListDrinksResponse res = sdk.drinks().listDrinks() .call(); if (res.drinks().isPresent()) { // handle response } } catch (dev.speakeasyapi.speakeasy.models.errors.SDKError e) { // handle exception } catch (Exception e) { // handle exception } } }`, }, { label: "C#", language: "csharp", code: `using Speakeasy; using Speakeasy.Models.Components; var sdk = new SDK( security: new Security() { ApiKey = "" } ); try { var res = await sdk.Drinks.ListDrinksAsync(); if (res.Drinks != null) { // handle response } } catch (Exception ex) { // handle exception }`, }, { label: "PHP", language: "php", code: `declare(strict_types=1); require 'vendor/autoload.php'; use OpenAPI\\OpenAPI; $sdk = OpenAPI\\SDK::builder() ->setSecurity( new OpenAPI\\Security( apiKey: "" ) ) ->build(); try { $response = $sdk->drinks->listDrinks(); if ($response->drinks !== null) { // handle response } } catch (Exception $e) { // handle exception }`, }, { label: "Ruby", language: "ruby", code: `require 'openapi' s = ::OpenApiSDK::SDK.new(security: ::OpenApiSDK::Models::Shared::Security.new(api_key: "")) begin res = s.drinks.list_drinks unless res.drinks.nil? # handle response end rescue APIError # handle exception end`, } ]} /> ### Per-operation security **Security hoisting:** In cases where global security is **not** defined, Speakeasy automatically hoists the most commonly occurring operation-level security to be considered global. This simplifies SDK usage. To opt out, set `auth.hoistGlobalSecurity: false` in your `gen.yaml`. Operation-specific security configurations override the default authentication settings for an endpoint. Per-operation configurations are commonly used for operations that do not require authentication or that are part of an authentication flow, such as for obtaining a short-lived access token. Define `security` within an operation's scope to apply operation-specific security: ```yaml paths: /drinks: get: operationId: listDrinks summary: Get a list of drinks. description: Get a list of drinks, if authenticated this will include stock levels and product codes otherwise it will only include public information. security: # Here - apiKey: [] tags: - drinks components: securitySchemes: api_key: type: apiKey name: api_key in: header security: - {} ``` In the resulting SDK, the user can pass in a specific security configuration as an argument to the method call: ") if res.drinks is not None: # handle response pass`, }, { label: "Go", language: "go", code: `package main import ( "context" "log" "speakeasy" "speakeasy/models/operations" ) func main() { s := speakeasy.New() operationSecurity := operations.ListDrinksSecurity{ APIKey: "", } ctx := context.Background() res, err := s.Drinks.ListDrinks(ctx, operationSecurity) if err != nil { log.Fatal(err) } if res.Drinks != nil { // handle response } }`, }, { label: "Java", language: "java", code: `package hello.world; import dev.speakeasyapi.speakeasy.SDK; import dev.speakeasyapi.speakeasy.models.components.*; import dev.speakeasyapi.speakeasy.models.components.Security; import dev.speakeasyapi.speakeasy.models.operations.*; import dev.speakeasyapi.speakeasy.models.operations.ListDrinksResponse; import java.math.BigDecimal; import java.math.BigInteger; import java.time.LocalDate; import java.time.OffsetDateTime; import java.util.Optional; import static java.util.Map.entry; public class Application { public static void main(String[] args) { try { SDK sdk = SDK.builder() .build(); ListDrinksResponse res = sdk.drinks().listDrinks() .security(ListDrinksSecurity.builder() .apiKey("") .build()) .call(); if (res.drinks().isPresent()) { // handle response } } catch (dev.speakeasyapi.speakeasy.models.errors.SDKError e) { // handle exception } catch (Exception e) { // handle exception } } }`, }, { label: "C#", language: "csharp", code: `using Speakeasy; using Speakeasy.Models.Components; var sdk = new SDK( security: new Security() { ApiKey = "" } ); try { var res = await sdk.Drinks.ListDrinksAsync(); if (res.Drinks != null) { // handle response } } catch (Exception ex) { // handle exception }`, }, { label: "PHP", language: "php", code: `declare(strict_types=1); require 'vendor/autoload.php'; use OpenAPI\\OpenAPI; $sdk = OpenAPI\\SDK::builder()->build(); $requestSecurity = new ListDrinksSecurity( apiKey: '', ); try { $response = $sdk->drinks->listDrinks( security: $requestSecurity, ); // handle response } catch (Exception $e) { // handle exception }`, }, { label: "Ruby", language: "ruby", code: `require 'openapi' Models = ::OpenApiSDK::Models s = ::OpenApiSDK::SDK.new begin res = s.drinks.list_drinks( security: Models::Shared::ListDrinksSecurity.new( api_key: '', ), ) unless res.drinks.nil? # handle response end rescue Models::Errors::APIError => e # handle exception raise e end`, } ]} /> ## Environment variables The global parameters and security options for an SDK are commonly set using environment variables. Speakeasy supports this pattern with environment variable prefixes. To enable this feature, set the `envVarPrefix` variable in the language section of the SDK's `gen.yaml` file. You can then provide global parameters via environment variables in the format `{PREFIX}_{GLOBALNAME}`. The relevant documentation will be automatically included in the README. Security options can also be set via environment variables if not provided directly to the SDK. For example, a security field like `api_key` can be set with the variable `{PREFIX}_{API_KEY}`: ```ts const SDK = new SDK({ apiKey: process.env["SDK_API_KEY"] ?? "", }); ``` In some cases, adding `envVarPrefix` may alter the structure of security options. Required global security will become optional to allow you to set it via environment variables. To learn more about enabling this feature during generation, see the language-specific `gen.yaml` [configuration reference](/docs/speakeasy-reference/generation/gen-yaml). # Custom Security Schemes Source: https://speakeasy.com/docs/sdks/customize/authentication/custom-security-schemes import { CodeWithTabs, Callout, Table } from "@/mdx/components"; Custom Security Schemes define a JSON Schema for SDK security options. Combined with [SDK Hooks](/docs/customize/code/sdk-hooks), custom authentication and authorization schemes can be implemented beyond OpenAPI's capabilities. Custom Security Schemes are only available for [Business and Enterprise users](/pricing). ### Language support
### Define a custom security scheme Define the custom security scheme under `components -> securitySchemes` and reference it in the `security` section. Set the `type` to `http` and the `scheme` to `custom`. Use the `x-speakeasy-custom-security-scheme` extension to specify a JSON Schema. This schema must include at least one property and can accommodate multiple properties with different schema definitions. ```yaml openapi: 3.1.0 info: title: Custom Security Scheme Example version: 1.0.0 security: - myCustomScheme: [] # reference to the custom security scheme defined below # ... components: securitySchemes: myCustomScheme: # defined as usual under components -> securitySchemes type: http scheme: custom # type: http, scheme: custom is used to identify the custom security scheme x-speakeasy-custom-security-scheme: # A JSON Schema is then provided via the x-speakeasy-custom-security-scheme extension schema: type: object # the JSON Schema MUST be defined as an object with at least one property, but can then have any number of properties with any schema properties: appId: type: string example: app-speakeasy-123 secret: type: string example: MTIzNDU2Nzg5MDEyMzQ1Njc4OTAxMjM0NTY3ODkwMTI required: - appId - secret ``` ### Initialize the custom security scheme Once the SDK is regenerated, the custom security scheme can be configured globally or per operation, depending on the `security` definitions. new Security() { AppId = "app-speakeasy-123", Secret = "MTIzNDU2Nzg5MDEyMzQ1Njc4OTAxMjM0NTY3ODkwMTI" });`, }, { label: "Java", language: "java", code: `import org.openapis.openapi.SDK; import org.openapis.openapi.models.components.Security; SDK sdk = SDK.builder() .security(Security.builder() .appId("app-speakeasy-123") .secret("MTIzNDU2Nzg5MDEyMzQ1Njc4OTAxMjM0NTY3ODkwMTI") .build()) .build();`, }, { label: "PHP", language: "php", code: `use Openapi; use Openapi\\Models\\Components; $sdk = Openapi\\SDK::builder() ->setSecurity( new Components\\Security( appId: 'app-speakeasy-123', secret: 'MTIzNDU2Nzg5MDEyMzQ1Njc4OTAxMjM0NTY3ODkwMTI', ) )->build();`, }, { label: "Ruby", language: "ruby", code: `require 'openapi' $sdk = Openapi\\SDK::builder() ->setSecurity( new Components\\Security( appId: 'app-speakeasy-123', secret: 'MTIzNDU2Nzg5MDEyMzQ1Njc4OTAxMjM0NTY3ODkwMTI', ) )->build();`, } ]} /> ### Use the custom security scheme Use [SDK Hooks](/docs/customize/code/sdk-hooks) to access user-provided security values and enable custom authentication workflows, like adding headers to requests. The following example illustrates accessing a custom security scheme in a hook and adding headers to a request: Union[requests.PreparedRequest, Exception]: security = hook_ctx.security_source if not security.app_id or not security.secret: raise ValueError("Missing security credentials") # Add security headers to the request request.headers["X-Security-App-Id"] = security.app_id request.headers["X-Security-Secret"] = security.secret return request`, }, { label: "Go", language: "go", code: `package hooks import ( "errors" "net/http" "openapi/models/components" ) type CustomSecurityHook struct{} func (h *CustomSecurityHook) BeforeRequest(hookCtx BeforeRequestContext, req *http.Request) (*http.Request, error) { // Access security values from hookCtx.Security security, ok := hookCtx.Security.(*components.Security) if !ok { return nil, errors.New("security context is not properly defined") } appId := security.GetAppID() secret := security.GetSecret() if appId == "" || secret == "" { return nil, errors.New("missing security credentials") } // Add security values to the request headers req.Header.Set("X-Security-App-Id", appId) req.Header.Set("X-Security-Secret", secret) return req, nil }`, }, { label: "C#", language: "csharp", code: `namespace Openapi.Hooks { using System; using System.Net.Http; using System.Net.Http.Headers; using System.Threading.Tasks; using Openapi.Models.Shared; public class CustomSecurityHook : IBeforeRequestHook { public async Task BeforeRequestAsync(BeforeRequestContext hookCtx, HttpRequestMessage request) { await Task.CompletedTask; if (hookCtx.SecuritySource == null) { throw new Exception("Security source is null"); } Security? security = hookCtx.SecuritySource() as Security; if (security == null) { throw new Exception("Unexpected security type"); } // Add security values to the request headers request.Headers.Add("X-Security-App-Id", security.AppId); request.Headers.Add("X-Security-Secret", security.Secret); return request; } } }`, }, { label: "Java", language: "java", code: `import java.net.http.HttpRequest; import org.openapis.openapi.models.shared.Security; import org.openapis.openapi.utils.Helpers; import org.openapis.openapi.utils.Hook.BeforeRequest; import org.openapis.openapi.utils.Hook.BeforeRequestContext; // an instance of this class is registered in SDKHooks public class CustomSecurityHook implements BeforeRequest { @Override public HttpRequest beforeRequest(BeforeRequestContext context, HttpRequest request) throws Exception { if (!context.securitySource().isPresent()) { throw new IllegalArgumentException("security source is not present"); } // this example is for global security, cast to the appropriate HasSecurity // implementation for method-level security Security sec = (Security) context.securitySource().get().getSecurity(); return Helpers.copy(request) // .header("X-Security-App-Id", sec.appId()) // .header("X-Security-Secret", sec.secret()) // .build(); } }`, }, { label: "PHP", language: "php", code: `namespace OpenAPI\\OpenAPI\\Hooks; use OpenAPI\\OpenAPI\\Models\\Shared\\Security; use Psr\\Http\\Message\\RequestInterface; class CustomSecurityHook implements BeforeRequestHook { public function beforeRequest(BeforeRequestContext $context, RequestInterface $request): RequestInterface { if ($context->securitySource === null) { throw new \\InvalidArgumentException('Security source is null'); } $security = $context->securitySource->call($context); if (! $security instanceof Security) { throw new \\InvalidArgumentException('Unexpected security type'); } $request = $request->withHeader('Idempotency-Key', 'some-key'); // Add security values to the request headers $request = $request->withAddedHeader('X-Security-App-Id', $security->appId); $request = $request->withAddedHeader('X-Security-Secret', $security->secret); return $request; } }`, }, { label: "Ruby", language: "ruby", code: `require_relative './types' require 'sorbet-runtime' module OpenApiSDK module SDKHooks class CustomSecurityHook extend T::Sig include AbstractBeforeRequestHook sig do override.params( hook_ctx: BeforeRequestHookContext, request: Faraday::Request ).returns(Faraday::Request) end def before_request(hook_ctx:, request:) raise ArgumentError, 'Security source is null' if hook_ctx.security_source.nil? security = hook_ctx.security_source.call raise ArgumentError, 'Unexpected security type' unless security.is_a?(Models::Shared::Security) # Add security values to the request headers request.headers['X-Security-App-Id'] = security.app_id request.headers['X-Security-Secret'] = security.secret request end end end end`, } ]} /> # OAuth 2.0 authentication Source: https://speakeasy.com/docs/sdks/customize/authentication/oauth import { CodeWithTabs, Table, Callout } from "@/mdx/components"; Speakeasy supports the OAuth 2.0 security implementation, including type generation for OAuth schemas and in many cases the complete management of the token refresh flow. End users of Speakeasy SDKs don't need to retrieve and manage access tokens manually. API builders also have the option to leverage Speakeasy's [custom security schemes](/docs/customize/authentication/custom-security-schemes) to implement custom OAuth flows that aren't part of the standard OpenAPI specification. This document covers the following types of OAuth 2.0 flows:
Other custom flows can be implemented using a combination of [hooks](/docs/customize/code/sdk-hooks) and [custom security schemes](/docs/customize/authentication/custom-security-schemes). ## Client credentials flow The client credentials flow is used to obtain an access token for API requests by prompting users for a client ID and client secret when instantiating the SDK. OAuth 2.0 defines several methods for building a request to the `tokenUrl` endpoint. Speakeasy supports the following authentication methods: - [`client_secret_post`](#client_secret_post) (default method) - [`client_secret_basic`](#client_secret_basic) - [`authorization-code`](#authorization-code-flow) (with hooks) - [Custom flows](#custom-refresh-token-flow) (with hooks and [custom security schemes](/docs/customize/authentication/custom-security-schemes)) To enable the client credentials flow in the SDK: - define `type: oauth2` and `flows: clientCredentials` in your OpenAPI specification. - add the following to the `gen.yaml` file: ```yaml configVersion: 2.0.0 generation: auth: OAuth2ClientCredentialsEnabled: true ``` ### client_secret_post
The `client_secret_post` method sends the client credentials in the request body as `application/x-www-form-urlencoded` form data. This is the default authentication method used by Speakeasy when no specific method is specified. ```yaml components: securitySchemes: clientCredentials: type: oauth2 flows: clientCredentials: tokenUrl: https://speakeasy.bar/oauth2/token/ # client_secret_post is the default method, so no additional configuration is needed scopes: {} ``` When using this method, the client ID and client secret are sent in the request body as form parameters: ``` client_id=YOUR_CLIENT_ID&client_secret=YOUR_CLIENT_SECRET&grant_type=client_credentials ``` ### client_secret_basic
The `client_secret_basic` method sends the client credentials in the `Authorization` header using the `Basic` authentication scheme. The client ID and client secret are combined with a colon separator, Base64-encoded, and sent in the header. To use this method, add the `x-speakeasy-token-endpoint-authentication: client_secret_basic` extension to your OAuth security scheme: ```yaml components: securitySchemes: clientCredentials: type: oauth2 flows: clientCredentials: tokenUrl: https://speakeasy.bar/oauth2/token/ x-speakeasy-token-endpoint-authentication: client_secret_basic scopes: {} ``` When using this method, the client ID and client secret are sent in the Authorization header: ``` Authorization: Basic base64(client_id:client_secret) ``` This method is preferred by some OAuth providers for security reasons, as it keeps credentials out of request bodies and server logs. ### tokenUrl The `tokenUrl` property in OAuth 2.0 flows can be specified in two formats: 1. A pathname (e.g., `/auth/token`) 2. An absolute URL (e.g., `https://api.example.com/auth/token`) When a pathname is provided instead of an absolute URL, Speakeasy resolves it relative to the user's configured server URL. This is particularly useful in environments where you have multiple server configurations and need the token endpoint to adapt accordingly. For example, if your OpenAPI specification includes a pathname: ```yaml components: securitySchemes: clientCredentials: type: oauth2 flows: clientCredentials: tokenUrl: /clientcredentials/token scopes: read: Read access write: Write access ``` And the user has configured their server URL as `https://api.example.com`, the effective token URL will be `https://api.example.com/clientcredentials/token`. Alternatively, you can specify an absolute URL: ```yaml components: securitySchemes: clientCredentials: type: oauth2 flows: clientCredentials: tokenUrl: https://auth.mycompany.com/clientcredentials/token scopes: read: Read access write: Write access ``` This feature allows for more flexible authentication configurations across different environments without requiring changes to the OpenAPI specification. Currently, absolute URLs are only supported in Go and Python. ### additional properties To provide additional properties to the `tokenUrl`, you need to use the `x-speakeasy-token-endpoint-additional-properties` extension. For example, to include an `audience` parameter we need to modify our OpenAPI specification like this: ```yaml components: securitySchemes: clientCredentials: type: oauth2 flows: clientCredentials: tokenUrl: /clientcredentials/token x-speakeasy-token-endpoint-additional-properties: audience: type: string example: "AUD" scopes: read: Read access write: Write access ``` Currently, `x-speakeasy-token-endpoint-additional-properties` are only supported in our primary targets: Go, TypeScript and Python. ### Example OpenAPI configuration Here's a complete example showing how to configure OAuth client credentials flow in your OpenAPI specification: ```yaml paths: /drinks: get: operationId: listDrinks summary: Get a list of drinks. security: - clientCredentials: - read:drinks tags: - drinks /drink/{id}: get: operationId: viewDrink summary: View drink details. tags: - drinks put: operationId: updateDrink summary: Update drink details. security: - clientCredentials: - write:drinks tags: - drinks components: securitySchemes: clientCredentials: type: oauth2 flows: clientCredentials: tokenUrl: /oauth2/token/ # Uncomment the following line to use client_secret_basic instead of the default client_secret_post # x-speakeasy-token-endpoint-authentication: client_secret_basic scopes: read:basic: Basic read access read:drinks: Allow listing available drinks write:drinks: Allow updating drinks inventory security: - clientCredentials: - read:basic ``` When making a token Request to the `tokenUrl`, all operation-specific scopes used in the specification will be requested alongside globally required scopes. Please refer to the [OAuth 2.0 scopes](#oauth2.0-scopes) section for more details.

", clientSecret: "", }, }); const result = await sdk.drinks.listDrinks(); // Handle the result console.log(result); } run();`, }, { label: "Python", language: "python", code: `import speakeasy from speakeasy.models.components import Security s = speakeasy.SDK( security=Security( client_id="", client_secret="", ), ) res = s.drinks.list_drinks() if res.drinks is not None: # handle response pass`, }, { label: "Go", language: "go", code: `package main import ( "context" "log" "speakeasy" "speakeasy/models/components" ) func main() { s := speakeasy.New( speakeasy.WithSecurity(components.Security{ ClientID: "", ClientSecret: "", }), ) ctx := context.Background() res, err := s.Drinks.ListDrinks(ctx) if err != nil { log.Fatal(err) } if res.Drinks != nil { // handle response } }`, }, { label: "Java", language: "java", code: `package hello.world; import dev.speakeasyapi.speakeasy.SDK; import dev.speakeasyapi.speakeasy.models.components.Security; import dev.speakeasyapi.speakeasy.models.operations.ListDrinksResponse; public class Application { public static void main(String[] args) { try { SDK sdk = SDK.builder() .security(Security.builder() .clientID("") .clientSecret("") .build()) .build(); ListDrinksResponse res = sdk.drinks().listDrinks() .call(); if (res.drinks().isPresent()) { // handle response } } catch (dev.speakeasyapi.speakeasy.models.errors.SDKError e) { // handle exception } catch (Exception e) { // handle exception } } }`, }, { label: "C#", language: "csharp", code: `using Speakeasy; using Speakeasy.Models.Components; var sdk = new SDK( security: new Security() { ClientID = "", ClientSecret = "" } ); try { var res = await sdk.Drinks.ListDrinksAsync(); if (res.Drinks != null) { // handle response } } catch (Exception ex) { // handle exception }`, }, { label: "PHP", language: "php", code: `use Speakeasy; use Speakeasy\\Models\\Components; use Speakeasy\\Models\\Operations; $sdk = Speakeasy\\SDK::builder() ->setSecurity( new Components\\Security( clientID: '', clientSecret: '', ) )->build(); try { $res = $sdk->drinks->listDrinks(); if ($res->Drinks != null) { // handle response } } catch (Errors\\ErrorThrowable $e) { // handle exception }`, } ]} /> ## Resource Owner Password Credentials flow Also known informally as OAuth 2.0 Password flow.
Resource Owner Password Credentials (ROPC) flow is designed for obtaining access tokens directly in exchange for a username and password. Below is an example of how ROPC Flow is configured in `openapi.yaml`. You'll note that `oauth2` security scheme is linked to the `listProducts` operation and that the scope `products:read` is required by the `listProducts` operation. ```yaml paths: /products: get: operationId: listProducts summary: List all products. responses: "200": description: Successful response. content: application/json: schema: $ref: "#/components/schemas/Products" components: securitySchemes: oauth2: type: oauth2 flows: password: tokenUrl: http://localhost:35456/oauth2/token scopes: products:read: Permission to read/list products products:create: Permission to create products products:delete: Permission to delete products admin: Full permission including managing product inventories security: - oauth2: [products:read] ``` To enable OAuth 2.0 ROPC flow in the SDK, add the following to the `gen.yaml` file: ```yaml configVersion: 2.0.0 generation: auth: OAuth2PasswordEnabled: true ``` When making a call using this flow, the SDK security is configured with these parameters:
Below are usage examples in supported languages: It is also possbile to bypass token retrievals by passing an explicit token to the SDK object: ## Authorization code flow Authorization code flows can vary in implementation, but there are typically some secret values that need to be passed during the code token exchange. The format for the secret values can also vary but a very common format is: : `, }, ]} /> - `` often being `Basic` or `Bearer` - The following string being some format of Client ID and Client Secret, combined with a `:` and then base64 encoded. To allow for any possible formatting Speakeasy offers support for Hooks, these hooks allow you to alter a request before it is sent to the server. For this example we will be using the names `id` and `secret`, but you can use any names you like. First we will define a custom security schema, documentation for that [can be found here](/docs/customize/authentication/custom-security-schemes) ```yaml tokenRequest: type: http scheme: custom x-speakeasy-custom-security-scheme: schema: properties: id: type: string example: app-speakeasy-123 secret: type: string example: MTIzNDU2Nzg5MDEyMzQ1Njc4OTAxMjM0NTY3ODkwMTI required: - id - secret description: The string `Basic` with your ID and Secret separated with colon (:), Base64-encoded. For example, Client_ID:Client_Secret Base64-encoded is Q2xpZW50X0lEOkNsaWVudF9TZWNyZXQ=. ``` This security schema will then be applied to our OAuth token exchange endpoint. ```yaml paths: /oauth/token: post: tags: - OAuth2 summary: OAuth2 Token description: Get an OAuth2 token for the API. operationId: getToken security: - tokenRequest: [] ``` This custom security schema allows us to supply the Id and Secret needed for the token exchange directly to that method, and generate the unique header value needed with a hook. Next we add the hook to generate that header. Now that the hook is added, when you are using the SDK to acquire an OAuth token, you can pass in the values and the hook will generate the special header for you. ## Custom refresh token flow To enable custom OAuth refresh token handling, implement [security callbacks](/docs/customize/authentication/security-callbacks) along with additional configuration outside the OpenAPI spec. ### Step 1: Define OAuth security in the OpenAPI spec ```yaml /oauth2/token: get: operationId: auth security: - [] responses: 200: description: OK content: application/json: schema: type: object properties: access_token: string required: - access_token /example: get: operationId: example responses: 200: description: OK components: securitySchemes: auth: type: oauth2 flows: clientCredentials: tokenUrl: https://speakeasy.bar/oauth2/token/ scopes: {} security: - auth: [] ``` ### Step 2: Add a callback function to the SDK Add a file called `oauth` with the appropriate file extension for the programming language (for example, `oauth.ts` for TypeScript, `oauth.py` for Python, `oauth.go` for Go, and so on) to implement OAuth token exchange logic. => { const session = await tokenStore.get(); // Return the current token if it has not expired yet. if (session && session.expires > Date.now()) { return session.token; } try { const response = await fetch(url, { method: "POST", headers: { "content-type": "application/x-www-form-urlencoded", // Include the SDK's user agent in the request so requests can be // tracked using observability infrastructure. "user-agent": SDK_METADATA.userAgent, }, body: new URLSearchParams({ client_id: clientID, client_secret: clientSecret, grant_type: "client_credentials", }), }); if (!response.ok) { throw new Error("Unexpected status code: " + response.status); } const json = await response.json(); const data = tokenResponseSchema.parse(json); await tokenStore.set( data.access_token, Date.now() + data.expires_in * 1000 - tolerance, ); return data.access_token; } catch (error) { throw new Error("Failed to obtain OAuth token: " + error); } }; } /** * A TokenStore is used to save and retrieve OAuth tokens for use across SDK * method calls. This interface can be implemented to store tokens in memory, * a shared cache like Redis or a database table. */ export interface TokenStore { get(): Promise<{ token: string; expires: number } | undefined>; set(token: string, expires: number): Promise; } /** * InMemoryTokenStore holds OAuth access tokens in memory for use by SDKs and * methods that require OAuth security. */ export class InMemoryTokenStore implements TokenStore { private token = ""; private expires = Date.now(); constructor() {} async get() { return { token: this.token, expires: this.expires }; } async set(token: string, expires: number) { this.token = token; this.expires = expires; } }`, }, { label: "TypeScript-Axios (v1)", language: "typescript", code: `import axios from "axios"; // eslint-disable-next-line @typescript-eslint/no-unused-vars export function withAuthorization(clientID: string, clientSecret: string) { return async (): Promise<{ auth: string }> => { const tokenEndpoint = "https://speakeasy.bar/oauth2/token/"; const data = { grant_type: "client_credentials", client_id: clientID, client_secret: clientSecret, }; try { const response = await axios.post(tokenEndpoint, data); return { auth: response.data.access_token }; } catch (error) { throw new Error("Failed to obtain OAuth token"); } }; }`, }, { label: "Python", language: "python", code: `import requests from sdk.components import Security def with_authorization(client_id: str, client_secret: str) -> Security: token_endpoint = 'https://speakeasy.bar/oauth2/token/' data = { 'grant_type': 'client_credentials', 'client_id': client_id, 'client_secret': client_secret, } try: response = requests.post(token_endpoint, data=data) response.raise_for_status() return Security(auth=response.json()['access_token']) except Exception as e: raise Exception(f'Failed to obtain OAuth token: {str(e)}')`, }, { label: "Go", language: "go", code: `package speakeasy import ( "speakeasy/components" ) func withAuthorization(clientID string, clientSecret string) func(context.Context) (components.Security, error) { return func(ctx context.Context) (components.Security, error) { // Please implement callback here return components.Security{Auth: ""}, nil } }`, }, { label: "Java", language: "java", code: `import dev.speakeasyapi.speakeasy.SecuritySource; import dev.speakeasyapi.speakeasy.models.components.Security; class OAuth implements SecuritySource { private String clientID; private String clientSecret; public OAuth(String clientID, String clientSecret) { this.clientID = clientID; this.clientSecret = clientSecret; } public Security getSecurity() { // Please implement callback here return Security.builder() .auth("") .build(); } }`, }, { label: "C#", language: "csharp", code: `namespace Speakeasy.Callbacks { using Speakeasy; using Speakeasy.Models.Components; public static class OAuth { public static Security withAuthorization(string clientID, string clientSecret) { // Please implement callback here return new Security { Auth = ""} } } } `, } ]} /> ### Step 3: Pass the callback function in SDK instantiation Update the README to show how to pass the callback function when instantiating the SDK: ", "")) res = s.drinks.list_drinks()`, }, { label: "Go", language: "go", code: `import ( "context" sdk "speakeasy" ) s := sdk.New( sdk.WithSecuritySource(withAuthorization( "", "", )), ) ctx := context.Background() s.Drinks.ListDrinks(ctx)`, }, { label: "Java", language: "java", code: `import dev.speakeasyapi.speakeasy.OAuth; import dev.speakeasyapi.speakeasy.SDK; OAuth securitySource = new OAuth("", ""); SDK s = SDK.builder() .securitySource(securitySource) .build(); ListDrinksResponse res = s.Drinks.listDrinks().call();`, }, { label: "C#", language: "csharp", code: `using Speakeasy; using Speakeasy.Callbacks; var sdk = new SDK(securitySource: OAuth.withAuthorization) var res = await sdk.Drinks.ListDrinksAsync();`, } ]} /> ## OAuth 2.0 scopes ### Global security with OAuth 2.0 scopes The _available_ scopes for the OAuth 2.0 scheme can be listed in the `scopes` property when defining the security component. ```yaml components: securitySchemes: oauth2: type: oauth2 flows: clientCredentials: tokenUrl: /oauth2/token/ scopes: read: Grants read access write: Grants write access ``` The following OpenAPI definition then applies _global_ OAuth 2.0 scopes: ```yaml security: - oauth2: - read # Apply the read scope globally - write # Apply the write scope globally ``` In this configuration the SDK automatically requests the `read` and `write` scopes for all operations. This is useful for APIs where most endpoints share the same level of access. When making a request, the SDK checks whether the token contains the required scopes for the operation. If the token lacks the necessary scopes or has expired, a new token is requested with the correct scopes. ### Per-operation OAuth 2.0 scheme For more control over specific API operations, OAuth2 security schemes can be applied to specific operations only: ```yaml paths: /drinks: get: operationId: listDrinks # uses API key authentication summary: Get a list of drinks. /order/{id}: get: operationId: viewOrder # uses OAuth2 client credentials flow summary: Get order details. security: - clientCredentials: [read] components: securitySchemes: apiKey: type: apiKey name: Authorization in: header clientCredentials: type: oauth2 flows: clientCredentials: tokenUrl: /oauth2/token/ scopes: read: Grants read access security: - apiKey: [] ``` ```typescript import { SDK } from "speakeasy"; const sdk = new SDK(); const result = await sdk.viewOrder({ security: { clientID: "", clientSecret: "", }, }); ``` ### Operation level OAuth 2.0 scopes Scopes defined in the root-level security section apply globally but can be overridden on a per-operation basis. In the following example: - the `read` scope is requested by default as it is defined in the root-level `security` section. - the `write` scope will be requested for the `updateOrder` operation only. - the `admin` scope is available in the `scopes` property but not actually used by any operation. ```yaml paths: /order/{id}: get: operationId: viewOrder summary: View order details. put: operationId: updateOrder summary: Update order details. security: - clientCredentials: [write] requestBody: required: true content: application/json: schema: $ref: "#/components/schemas/Order" components: securitySchemes: clientCredentials: type: oauth2 flows: clientCredentials: tokenUrl: /oauth2/token/ scopes: read: Grants read access write: Grants write access admin: Grants admin access security: - clientCredentials: [read] ``` ### User-defined OAuth 2.0 scopes By enabling the `x-speakeasy-overridable-scopes` extension, end users can override the list of scopes sent to the authorization server for each token request. The `x-speakeasy-overridable-scopes` extension is currently only supported for the `clientCredentials` flow. First, add `x-speakeasy-overridable-scopes: true` to the `clientCredentials` flow definition: ```yaml components: securitySchemes: clientCredentials: type: oauth2 flows: clientCredentials: tokenUrl: /oauth2/token/ x-speakeasy-overridable-scopes: true scopes: read: Grants read access write: Grants write access admin: Grants admin access security: - clientCredentials: [read, write] ``` Users can then specify a custom list of scopes at runtime by setting the `scopes` field when instantiating the security object. When provided, these scopes will take precedence over the ones defined in the OpenAPI specification. ```typescript async function run() { const sdk = new SDK({ security: { clientID: "", clientSecret: "", scopes: ["user:read", "user:write"], }, }); } ``` # Security and authentication Source: https://speakeasy.com/docs/sdks/customize/authentication/overview import { Table } from "@/mdx/components"; ## Authentication overview Speakeasy-created SDKs have authentication automatically configured based on the `securitySchemes` defined in the [OpenAPI specification](/openapi/security). APIs authenticated with simple schemes, such as Basic HTTP auth, API keys, and bearer tokens, work out of the box. For APIs using short-lived tokens (OAuth), additional configuration is required to simplify setup.
# Security callbacks Source: https://speakeasy.com/docs/sdks/customize/authentication/security-callbacks import { CodeWithTabs, Table } from "@/mdx/components"; Instead of providing credentials once during SDK instantiation, pass a custom authentication function that allows end users to manage secrets dynamically. Custom authentication functions can be used to automatically refresh tokens or retrieve secrets from a secret store. ## Language support
## Example: Bearer authentication In this example, bearer authentication is used as the only security scheme: ```yaml security: - bearerAuth: [] components: securitySchemes: bearerAuth: type: http scheme: bearer ``` The callback function passed when initializing the SDK acts as a _security source_ and is called whenever a request is made, allowing tokens to be refreshed if needed. "; import { Security } from "/models"; const sdk = new SDK({ security: async (): Promise => { // refresh token here const token = ""; return { bearerAuth: token }; }, });`, }, { label: "Python", language: "python", code: `import requests import sdk from sdk.components import Security def callback() -> Security: # refresh token here token = "" return Security(bearer_auth=token) s = sdk.SDK(security=with_authorization(callback))`, }, { label: "Go", language: "go", code: `import ( "context" sdk "speakeasy" "speakeasy/components" ) s := sdk.New( sdk.WithSecuritySource(func(ctx context.Context) (components.Security, error) { // refresh token here token := "" return components.Security{BearerAuth: token}, nil }), )`, }, { label: "Java", language: "java", code: `import dev.speakeasyapi.speakeasy.SDK; import dev.speakeasyapi.speakeasy.SecuritySource; import dev.speakeasyapi.speakeasy.models.components.Security; class BearerSource implements SecuritySource { public Security getSecurity() { // refresh token here return Security.builder() .bearerAuth("") .build(); } } _____________________________________ import dev.speakeasyapi.speakeasy.SDK; import dev.speakeasyapi.speakeasy.SecuritySource; import dev.speakeasyapi.speakeasy.models.components.Security; SDK s = SDK.builder() .securitySource(new BearerSource()) .build();`, }, { label: "C#", language: "csharp", code: `using Speakeasy; using Speakeasy.Models.Components; Func tokenSource = () => { // refresh token here var token = "" return new Security { BearerAuth = token} } var sdk = new SDK(securitySource: tokenSource);`, }, { label: "PHP", language: "php", code: `use Speakeasy\\Speakeasy; $sdk = Speakeasy\\SDK::builder() ->setSecuritySource( function (): Security { //refresh token here var token = ""; return new Security(bearerAuth: $token); } )->build(); try { $res = $sdk->drinks->listDrinks(); if ($res->Drinks != null) { // handle response } } catch (Errors\\ErrorThrowable $e) { // handle exception }`, }, { label: "Ruby", language: "ruby", code: `require 'speakeasy' sdk = Speakeasy::SDK.new( security_source: -> { # Refresh token here token = '' Models::Components::Security.new(bearer_auth: token) } ) res = sdk.drinks.list_drinks`, } ]} /> # Simple security schemes Source: https://speakeasy.com/docs/sdks/customize/authentication/simple-schemes import { CodeWithTabs } from "@/mdx/components"; ## Basic HTTP authentication Basic HTTP authentication is supported in all languages. Define `type: http` and `scheme: basic` to generate authentication that prompts users for a username and password when instantiating the SDK. The SDK will encode the username and password into a Base64 string and pass it in the `Authorization` header. ```yaml paths: /drinks: get: operationId: listDrinks summary: Get a list of drinks. description: Get a list of drinks, if authenticated this will include stock levels and product codes otherwise it will only include public information. tags: - drinks components: securitySchemes: auth: type: http scheme: basic security: - auth: [] ``` ", password: "", }, }); const result = await sdk.drinks.listDrinks(); // Handle the result console.log(result); } run();`, }, { label: "Python", language: "python", code: `import speakeasy from speakeasy.models import components s = speakeasy.SDK( security=components.Security( username="", password="", ), ) res = s.drinks.list_drinks() if res.drinks is not None: # handle response pass`, }, { label: "Go", language: "go", code: `package main import ( "context" "log" "speakeasy" "speakeasy/models/components" ) func main() { s := speakeasy.New( speakeasy.WithSecurity(components.Security{ Username: "", Password: "", }), ) ctx := context.Background() res, err := s.Drinks.ListDrinks(ctx) if err != nil { log.Fatal(err) } if res.Drinks != nil { // handle response } }`, }, { label: "Java", language: "java", code: `package hello.world; import dev.speakeasyapi.speakeasy.SDK; import dev.speakeasyapi.speakeasy.models.components.*; import dev.speakeasyapi.speakeasy.models.components.Security; import dev.speakeasyapi.speakeasy.models.operations.*; import dev.speakeasyapi.speakeasy.models.operations.ListDrinksResponse; import java.math.BigDecimal; import java.math.BigInteger; import java.time.LocalDate; import java.time.OffsetDateTime; import java.util.Optional; import static java.util.Map.entry; public class Application { public static void main(String[] args) { try { SDK sdk = SDK.builder() .security(Security.builder() .username("") .password("") .build()) .build(); ListDrinksResponse res = sdk.drinks().listDrinks() .call(); if (res.drinks().isPresent()) { // handle response } } catch (dev.speakeasyapi.speakeasy.models.errors.SDKError e) { // handle exception } catch (Exception e) { // handle exception } } }`, }, { label: "C#", language: "csharp", code: `using Speakeasy; using Speakeasy.Models.Components; var sdk = new SDK( security: new Security() { Username = "", Password = "", } ); try { var res = await sdk.Drinks.ListDrinksAsync(); if (res.Drinks != null) { // handle response } } catch (Exception ex) { // handle exception }`, }, { label: "PHP", language: "php", code: `use OpenAPI\\OpenAPI; use OpenAPI\\OpenAPI\\Models\\Shared; $sdk = OpenAPI\\SDK::builder()->setSecurity( new Shared\\Security( password: 'YOUR_PASSWORD', username: 'YOUR_USERNAME', ); )->build(); try { $res = $sdk->drinks->listDrinks(); if ($res->Drinks != null) { // handle response } } catch (Errors\\ErrorThrowable $e) { // handle exception }`, }, { label: "Ruby", language: "ruby", code: `require 'openapi' Models = ::OpenApiSDK::Models s = ::OpenApiSDK::SDK.new( security: Models::Shared::Security.new( username: 'YOUR_USERNAME', password: 'YOUR_PASSWORD', ), ) begin res = s.drinks.list_drinks unless res.drinks.nil? # handle response end rescue Models::Errors::APIError => e # handle exception raise e end `, } ]} /> ## API key authentication API key authentication is supported in all languages. Define `type: apiKey` and `in: [header,query]` to generate authentication that prompts users for a key when instantiating the SDK. The SDK passes the key in a header or query parameter, depending on the `in` property, and uses the `name` field as the header or key name. ```yaml paths: /drinks: get: operationId: listDrinks summary: Get a list of drinks. description: Get a list of drinks, if authenticated this will include stock levels and product codes otherwise it will only include public information. tags: - drinks responses: "200": description: OK #... components: securitySchemes: api_key: type: apiKey name: api_key in: header security: - api_key: [] ``` ", }); const result = await sdk.drinks.listDrinks(); // Handle the result console.log(result); } run();`, }, { label: "Python", language: "python", code: `import speakeasy s = speakeasy.SDK( api_key="", ) res = s.drinks.list_drinks() if res.drinks is not None: # handle response pass`, }, { label: "Go", language: "go", code: `package main import ( "context" "log" "speakeasy" "speakeasy/models/components" ) func main() { s := speakeasy.New( speakeasy.WithSecurity(""), ) ctx := context.Background() res, err := s.Drinks.ListDrinks(ctx) if err != nil { log.Fatal(err) } if res.Drinks != nil { // handle response } }`, }, { label: "Java", language: "java", code: `package hello.world; import dev.speakeasyapi.speakeasy.SDK; import dev.speakeasyapi.speakeasy.models.components.*; import dev.speakeasyapi.speakeasy.models.components.Security; import dev.speakeasyapi.speakeasy.models.operations.*; import dev.speakeasyapi.speakeasy.models.operations.ListDrinksResponse; import java.math.BigDecimal; import java.math.BigInteger; import java.time.LocalDate; import java.time.OffsetDateTime; import java.util.Optional; import static java.util.Map.entry; public class Application { public static void main(String[] args) { try { SDK sdk = SDK.builder() .apiKey("") .build(); ListDrinksResponse res = sdk.drinks().listDrinks() .call(); if (res.drinks().isPresent()) { // handle response } } catch (dev.speakeasyapi.speakeasy.models.errors.SDKError e) { // handle exception } catch (Exception e) { // handle exception } } }`, }, { label: "C#", language: "csharp", code: `using Speakeasy; using Speakeasy.Models.Components; var sdk = new SDK( security: new Security() { ApiKey = "" } ); try { var res = await sdk.Drinks.ListDrinksAsync(); if (res.Drinks != null) { // handle response } } catch (Exception ex) { // handle exception }`, }, { label: "PHP", language: "php", code: `use OpenAPI\\OpenAPI; use OpenAPI\\OpenAPI\\Models\\Shared; $sdk = OpenAPI\\SDK::builder()->setSecurity( new Shared\\Security( apiKey: "" ); )->build(); try { $res = $sdk->drinks->listDrinks(); if ($res->Drinks != null) { // handle response } } catch (Errors\\ErrorThrowable $e) { // handle exception }`, }, { label: "Ruby", language: "ruby", code: `require 'openapi' Models = ::OpenApiSDK::Models s = ::OpenApiSDK::SDK.new( security: Models::Shared::Security.new( api_key: '', ), ) begin res = s.drinks.list_drinks unless res.drinks.nil? # handle response end rescue Models::Errors::APIError => e # handle exception raise e end`, } ]} /> ## Bearer token authentication Bearer token authentication is supported in all languages. Define `type: http` and `scheme: bearer` to generate authentication that prompts users for a token when instantiating the SDK. The SDK will pass the token in the `Authorization` header using the `Bearer` scheme, appending the `Bearer` prefix to the token if not already present. ```yaml paths: /drinks: get: operationId: listDrinks summary: Get a list of drinks. description: Get a list of drinks, if authenticated this will include stock levels and product codes otherwise it will only include public information. tags: - drinks components: securitySchemes: auth: type: http scheme: bearer security: - auth: [] ``` ", }); const result = await sdk.drinks.listDrinks(); // Handle the result console.log(result); } run();`, }, { label: "Python", language: "python", code: `import speakeasy s = speakeasy.SDK( auth="", ) res = s.drinks.list_drinks() if res.drinks is not None: # handle response pass`, }, { label: "Go", language: "go", code: `package main import ( "context" "log" "speakeasy" "speakeasy/models/components" ) func main() { s := speakeasy.New( speakeasy.WithSecurity(""), ) ctx := context.Background() res, err := s.Drinks.ListDrinks(ctx) if err != nil { log.Fatal(err) } if res.Drinks != nil { // handle response } }`, }, { label: "Java", language: "java", code: `package hello.world; import dev.speakeasyapi.speakeasy.SDK; import dev.speakeasyapi.speakeasy.models.components.*; import dev.speakeasyapi.speakeasy.models.components.Security; import dev.speakeasyapi.speakeasy.models.operations.*; import dev.speakeasyapi.speakeasy.models.operations.ListDrinksResponse; import java.math.BigDecimal; import java.math.BigInteger; import java.time.LocalDate; import java.time.OffsetDateTime; import java.util.Optional; import static java.util.Map.entry; public class Application { public static void main(String[] args) { try { SDK sdk = SDK.builder() .auth("") .build(); ListDrinksResponse res = sdk.drinks().listDrinks() .call(); if (res.drinks().isPresent()) { // handle response } } catch (dev.speakeasyapi.speakeasy.models.errors.SDKError e) { // handle exception } catch (Exception e) { // handle exception } } }`, }, { label: "C#", language: "csharp", code: `using Speakeasy; using Speakeasy.Models.Components; var sdk = new SDK( security: new Security() { Auth = "" } ); try { var res = await sdk.Drinks.ListDrinksAsync(); if (res.Drinks != null) { // handle response } } catch (Exception ex) { // handle exception }`, }, { label: "PHP", language: "php", code: `use OpenAPI\\OpenAPI; use OpenAPI\\OpenAPI\\Models\\Components; $sdk = OpenAPI\\SDK::builder()->setSecurity( new Components\\Security( auth: "" ); )->build(); try { $res = $sdk->drinks->listDrinks(); if ($res->Drinks != null) { // handle response } } catch (Errors\\ErrorThrowable $e) { // handle exception }`, }, { label: "Ruby", language: "ruby", code: `require 'openapi' Models = ::OpenApiSDK::Models s = ::OpenApiSDK::SDK.new( security: Models::Shared::Security.new( auth: '', ), ) begin res = s.drinks.list_drinks unless res.drinks.nil? # handle response end rescue Models::Errors::APIError => e # handle exception raise e end`, } ]} /> # Customization basics Source: https://speakeasy.com/docs/sdks/customize/basics import { Callout } from "@/mdx/components"; The Speakeasy SDK pipeline uses sensible defaults to generate SDKs, but various customizations can improve the user experience. Customizations can be applied using the following methods: 1. Modifying the OpenAPI document. 2. Adding `x-speakeasy` extensions to the OpenAPI document. 3. Editing the `gen.yaml` file in the SDK repository. --- ## 1. Modifying the OpenAPI document The OpenAPI document is the foundation of SDK generation. Modifications to the OpenAPI document influence the structure, naming conventions, and functionality of generated SDKs. Learn more about OpenAPI in [the reference documentation](/openapi). ### Modifying the OpenAPI document with overlays Speakeasy supports OpenAPI overlays for customizing and extending existing OpenAPI documents without directly modifying them. Overlays are especially useful for applying different configurations or updates to the specification for various environments or SDKs without altering the base OpenAPI document. Overlays work by referencing and extending parts of the base OpenAPI document. They can be used to add, override, or remove elements such as paths, schemas, parameters, or security configurations. ![Screenshot showing a speakeasy extension in VS code editor.](/assets/docs/overlay.png) [Learn more about overlays](/docs/prep-openapi/overlays/create-overlays). --- ## 2. Using x-speakeasy extensions Proprietary Speakeasy extensions provide fine-tuned control over the SDK, enabling modification of behaviors such as retries, pagination, error handling, and other advanced SDK features. Add Speakeasy extensions to the OpenAPI document. ![Screenshot showing a speakeasy extension in VS code editor.](/assets/docs/speakeasy-extensions.png) For a complete list of available extensions, see the [Speakeasy extensions reference](/docs/speakeasy-extensions). --- ## 3. Editing the gen.yaml file Further customize Speakeasy-generated SDKs by editing the `gen.yaml` file, typically located in the `.speakeasy` folder at the root of the SDK. This configuration file contains both language-agnostic and language-specific settings, offering more control over the structure and behavior of the SDK beyond what the OpenAPI document provides. Edit the `gen.yaml` file to modify elements like class names, method parameters, and response formatting. ![Screenshot showing the gen.yaml in VS code editor.](/assets/docs/genyaml.png) For a complete list of available options, refer to the [`gen.yaml` reference](/docs/gen-reference). --- ## 4. Adding custom code to SDKs Speakeasy provides two approaches for adding custom code to generated SDKs: ### SDK hooks [SDK hooks](/docs/sdks/customize/code/sdk-hooks) enable custom logic at various points in the SDK request lifecycle, including SDK initialization, before requests, after successful responses, and after errors. Best for: - Custom authentication and security schemes - Request/response transformations - Tracing and logging - HTTP client customization ### Custom code [Custom code](/docs/sdks/customize/code/custom-code/custom-code) allows custom changes anywhere in the generated SDK. Modifications are automatically preserved across regenerations using intelligent 3-way merging. Best for: - Adding utility methods to models - Extending SDK initialization - Extending SDK methods (such as interactions hard to model with OpenAPI) --- ## SDK dependencies Speakeasy-generated SDKs use dependencies and tools selected based on extensive testing and best practices for each language ecosystem. ### Dependency version management Speakeasy does not provide an out-of-the-box way to change the versions of dependencies that are automatically added to generated SDKs. This design decision ensures that: - All SDKs use dependencies thoroughly tested with the generator - Compatibility issues between dependencies are minimized - Security vulnerabilities in dependencies can be addressed systematically For specific dependency versions, the recommended approach is: 1. **Post-generation scripts**: Implement a post-generation build process that modifies dependency specifications before building or publishing the SDK. This can be integrated into the CI/CD pipeline to run automatically after SDK generation. For example, create a script that: - Parses and modifies package.json, requirements.txt, go.mod, or other dependency files - Updates specific dependency versions to meet requirements - Runs before the build or publish step in the workflow 2. **Contact Speakeasy support**: For enterprise customers with specific dependency requirements, contact [Speakeasy support](/support/enterprise) to discuss custom solutions. Modifying dependency versions may lead to compatibility issues or unexpected behavior. Proceed with caution and thorough testing. # Custom code regions in Java Source: https://speakeasy.com/docs/sdks/customize/code/code-regions/java To enable custom code regions for Java SDKs, update the project's `.speakeasy/gen.yaml` file as follows: ```diff filename=".speakeasy/gen.yaml" configVersion: 2.0.0 generation: # ... java: # ... + enableCustomCodeRegions: true ``` ## Full example The Speakeasy examples repository includes a [full Java SDK](https://github.com/speakeasy-api/examples/tree/main/customcode-sdkclasses-java) that uses custom code regions. ## Regions Below are the available code regions in Java SDKs. ### SDK classes Java SDK classes can have two code regions: - `// #region imports`: The imports region allows you to add imports to an SDK file needed for custom methods and properties. It must be located at the top of the file alongside generated imports. - `// #region class-body`: The class-body region allows you to add custom methods and properties to an SDK class. It must be located in the body of a Java SDK class alongside generated methods and properties. ### Model classes Java model classes can also have custom code regions: - `// #region imports`: The imports region allows you to add imports to a model file needed for custom methods and properties. It must be located at the top of the file alongside generated imports. - `// #region class-body`: The class-body region allows you to add custom methods and properties to a model class. It must be located in the body of a Java model class alongside generated methods and properties. ## Managing dependencies When adding custom code that requires external packages, configure these dependencies in the `.speakeasy/gen.yaml` file to prevent them from being removed during SDK regeneration. Use the `additionalDependencies` configuration to specify package dependencies: ```yaml filename=".speakeasy/gen.yaml" java: additionalDependencies: - implementation:org.commonmark:commonmark:0.21.0 - implementation:org.jsoup:jsoup:1.16.1 - testImplementation:org.junit.jupiter:junit-jupiter:5.9.2 - testImplementation:org.mockito:mockito-core:5.1.1 ``` This ensures that dependencies persist across SDK regenerations and are properly included in the generated `build.gradle`. ```java filename="src/main/java/com/example/sdk/Todos.java" /* * Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT. */ package com.example.sdk; import com.example.sdk.models.operations.*; import com.example.sdk.models.shared.*; import com.example.sdk.utils.HTTPClient; import com.example.sdk.utils.HTTPRequest; import com.example.sdk.utils.Utils; // #region imports import org.commonmark.parser.Parser; import org.commonmark.renderer.html.HtmlRenderer; // #endregion imports public class Todos implements MethodCallCreate, MethodCallGetOne { private final SDKConfiguration sdkConfiguration; Todos(SDKConfiguration sdkConfiguration) { this.sdkConfiguration = sdkConfiguration; } // #region class-body public String renderTodo(String id) throws Exception { Todo todo = getOne(id).todo() .orElseThrow(() -> new Exception("Todo not found")); Parser parser = Parser.builder().build(); HtmlRenderer renderer = HtmlRenderer.builder().build(); String markdown = String.format("# %s\n\n%s", todo.title(), todo.description()); return renderer.render(parser.parse(markdown)); } // #endregion class-body public Todo getOne(String id) throws Exception { // Generated method implementation ... } } ``` ## Model class example You can also add custom methods to model classes. This example adds a `render()` method to a `Todo` model class: ```java filename="src/main/java/com/example/sdk/models/components/Todo.java" /* * Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT. */ package com.example.sdk.models.components; import java.util.Objects; // #region imports import org.commonmark.parser.Parser; import org.commonmark.renderer.html.HtmlRenderer; // #endregion imports import com.example.sdk.utils.Utils; import com.fasterxml.jackson.annotation.JsonCreator; import com.fasterxml.jackson.annotation.JsonIgnore; import com.fasterxml.jackson.annotation.JsonProperty; public class Todo { @JsonProperty("id") private String id; @JsonProperty("title") private String title; @JsonProperty("description") private String description; @JsonProperty("completed") private boolean completed; @JsonCreator public Todo( @JsonProperty("id") String id, @JsonProperty("title") String title, @JsonProperty("description") String description, @JsonProperty("completed") boolean completed) { Utils.checkNotNull(id, "id"); Utils.checkNotNull(title, "title"); Utils.checkNotNull(description, "description"); Utils.checkNotNull(completed, "completed"); this.id = id; this.title = title; this.description = description; this.completed = completed; } @JsonIgnore public String id() { return id; } @JsonIgnore public String title() { return title; } @JsonIgnore public String description() { return description; } @JsonIgnore public boolean completed() { return completed; } // #region class-body public String render() throws Exception { Parser parser = Parser.builder().build(); HtmlRenderer renderer = HtmlRenderer.builder().build(); String markdown = String.format("# %s\n\n%s", title(), description()); return renderer.render(parser.parse(markdown)); } // #endregion class-body // Generated methods continue... } ``` This allows you to use the custom method as follows: ```java Todo todo = ...; String rendered = todo.render(); ``` # Custom code regions Source: https://speakeasy.com/docs/sdks/customize/code/code-regions/overview import { Callout } from "@/mdx/components"; Looking for more flexibility? Check out [Custom code](/docs/sdks/customize/code/custom-code/custom-code) - a feature that allows custom changes anywhere in the SDK, not just in predefined regions. Generally, the Speakeasy code generator "owns" the files it generates. Modifying these files causes the next generation run to overwrite all edits. One way to persist modifications to generated files is to add them to `.genignore`, but this has a significant drawback: those files will stop receiving updates during generation, and thus risk build failures in the future. **Custom code regions** allow developers to add code to specific sections of a generated file that the generator knows to carry forward. Speakeasy can continue to own and update files while providing a constrained way to add bespoke functionality to SDKs. ## Syntax Custom code regions are defined by adding start and end comments to prescribed sections of a generated file. The comments follow Visual Studio Code's format for [creating code folds](https://code.visualstudio.com/docs/editor/codebasics#_folding). ## Language support Custom code regions are currently supported in the following languages: - [Java](/docs/sdks/customize/code/code-regions/java) - [Python](/docs/sdks/customize/code/code-regions/python) - [TypeScript](/docs/sdks/customize/code/code-regions/typescript) Custom code regions are only available for [Enterprise users](/pricing). Custom code regions must be enabled in `settings/billing` under the account. # Custom code regions in Python Source: https://speakeasy.com/docs/sdks/customize/code/code-regions/python To enable custom code regions for Python SDKs, update the project's `.speakeasy/gen.yaml` file as follows: ```diff filename=".speakeasy/gen.yaml" configVersion: 2.0.0 generation: # ... python: # ... + enableCustomCodeRegions: true ``` ## Full example The Speakeasy examples repository includes a [full Python SDK](https://github.com/speakeasy-api/examples/tree/main/customcode-sdkclasses-python) that uses custom code regions. ## Regions Below are the available code regions in Python SDKs. ### SDK classes Python SDK classes can have two code regions: - `# region imports`: The imports region allows you to add imports to an SDK file needed for custom methods and properties. It must be located at the top of the file alongside generated imports. - `# region sdk-class-body`: The class-body region allows you to add custom methods and properties to an SDK class. It must be located in the body of a Python SDK class alongside generated methods and properties. ## Managing dependencies When adding custom code that requires external packages, configure these dependencies in the `.speakeasy/gen.yaml` file to prevent them from being removed during SDK regeneration. Use the `additionalDependencies` configuration to specify package dependencies: ```yaml filename=".speakeasy/gen.yaml" python: additionalDependencies: main: markdown: "^3.4.0" beautifulsoup4: "^4.12.0" dev: pytest: "^7.0.0" black: "^23.0.0" ``` This ensures that dependencies persist across SDK regenerations and are properly included in the generated `pyproject.toml`. ```python filename="src/todos_sdk/todos.py" """Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT.""" from .basesdk import BaseSDK from todos_sdk import models, utils from todos_sdk._hooks import HookContext from todos_sdk.types import OptionalNullable, UNSET from typing import Mapping, Optional # region imports import markdown # endregion imports class Todos(BaseSDK): # region sdk-class-body def render_todo(self, id: str) -> str: todo = self.get_one(id=id) return markdown.markdown(f"# {todo.title}\n\n{todo.description}") # endregion sdk-class-body def get_one( self, *, id: int, retries: OptionalNullable[utils.RetryConfig] = UNSET, server_url: Optional[str] = None, timeout_ms: Optional[int] = None, http_headers: Optional[Mapping[str, str]] = None, ) -> models.Todo: ... async def get_one_async( self, *, id: int, retries: OptionalNullable[utils.RetryConfig] = UNSET, server_url: Optional[str] = None, timeout_ms: Optional[int] = None, http_headers: Optional[Mapping[str, str]] = None, ) -> models.Todo: ... ``` # Custom code regions in TypeScript Source: https://speakeasy.com/docs/sdks/customize/code/code-regions/typescript To enable custom code regions for TypeScript SDKs, update the project's `.speakeasy/gen.yaml` file as follows: ```diff filename=".speakeasy/gen.yaml" configVersion: 2.0.0 generation: # ... typescript: # ... + enableCustomCodeRegions: true ``` ## Full example The Speakeasy examples repository includes a [full TypeScript SDK](https://github.com/speakeasy-api/examples/tree/main/customcode-sdkclasses-typescript) that uses custom code regions. ## Regions Below are the available code regions in TypeScript SDKs. ### SDK classes TypeScript SDK classes can have two code regions: - `// #region imports`: The imports region allows you to add imports to an SDK file needed for custom methods and properties. It must be located at the top of the file alongside generated imports. - `// #region sdk-class-body`: The class-body region allows you to add custom methods and properties to an SDK class. It must be located in the body of a TypeScript SDK class alongside generated methods and properties. ## Managing dependencies When adding custom code that requires external packages, configure these dependencies in the `.speakeasy/gen.yaml` file to prevent them from being removed during SDK regeneration. Use the `additionalDependencies` configuration to specify package dependencies: ```yaml filename=".speakeasy/gen.yaml" typescript: additionalDependencies: dependencies: marked: "^5.0.0" dompurify: "^3.0.0" devDependencies: "@types/dompurify": "^3.0.0" peerDependencies: react: "^18.0.0" ``` This ensures that dependencies persist across SDK regenerations and are properly included in the generated `package.json`. ```typescript filename="src/sdk/todos.ts" /* * Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT. */ import { todosGetOne } from "../funcs/todosGetOne.js"; import { ClientSDK, RequestOptions } from "../lib/sdks.js"; import * as operations from "../models/operations/index.js"; import { unwrapAsync } from "../types/fp.js"; // #region imports import DOMPurify from "dompurify"; import marked from "marked"; import type { Todo } from "../models/components/todo.js"; // #endregion imports export class Todos extends ClientSDK { // #region sdk-class-body async renderTodo(id: string): Promise { const todo = await this.getOne({ id }); const html = await marked.parse(`# ${todo.title}\n\n${todo.description}`, { async: true, }); return DOMPurify.sanitize(html); } // #endregion sdk-class-body async getOne( request: operations.TodosGetOneRequest, options?: RequestOptions, ): Promise { return await unwrapAsync(todosGetOne(this, request, options)); } } ``` # Custom code best practices Source: https://speakeasy.com/docs/sdks/customize/code/custom-code/custom-code-best-practices import { Callout } from "@/mdx/components"; This guide covers best practices for using custom code effectively, avoiding common pitfalls, and working smoothly in team environments. ## When to use custom code ### Good use cases Custom code works best for: - **Adding utility methods** to models or SDK classes - **Extending initialization** with custom authentication or middleware - **Modifying configuration files** like package.json or pyproject.toml - **Adding business logic** specific to the domain - **Performance optimizations** that require deep changes - **Integration code** for internal systems ### When to consider alternatives Consider other approaches when: - **OpenAPI can solve it**: Many customizations can be handled via OpenAPI extensions - **Hooks suffice**: [SDK hooks](/docs/sdks/customize/code/sdk-hooks) might provide enough flexibility if you want to simply alter or act on the request or response. They are also useful for custom authentication setups ## Avoiding conflicts ### Structure changes to minimize conflicts **Delegate logic to separate files** Keep changes in generated files minimal to reduce merge conflicts. ```typescript // ❌ Avoid: Writing complex logic directly in generated files export class PaymentSDK { async createPayment(data: PaymentRequest): Promise { // Generated code... // 50 lines of custom validation logic mixed in... if (!data.amount || data.amount < 0) { // ...complex validation... } // More generated code... } } // ✅ Better: Import and call external logic import { validatePayment } from "./custom/validator"; // Only 1 line added export class PaymentSDK { async createPayment(data: PaymentRequest): Promise { validatePayment(data); // Only 1 line added // Generated code continues unchanged... } } ``` **Add methods, do not modify existing ones** ```typescript // ❌ Avoid: Modifying generated methods class User { // This method is generated getName(): string { // Changed the implementation return this.firstName + " " + this.lastName; } } // ✅ Better: Add new methods class User { // Generated method untouched getName(): string { return this.name; } // Custom method addition getFullName(): string { return this.firstName + " " + this.lastName; } } ``` ## Team workflows ### Communicating changes **Document customizations** Create a `CUSTOMIZATIONS.md` file in the SDK: ```markdown filename="CUSTOMIZATIONS.md" # SDK Customizations This SDK has custom code enabled. The following customizations have been added: ## Utility Methods - `Payment.toInvoiceItem()` - Converts payments to invoice format - `User.getFullName()` - Returns formatted full name ## Custom Dependencies - `aws-sdk` - For S3 upload functionality - `redis` - For caching API responses ## Modified Files - `src/models/payment.ts` - Added utility methods - `package.json` - Added custom dependencies and scripts ``` **Use clear commit messages** ```bash # When adding customizations git commit -m "feat(sdk): add payment utility methods for invoice conversion" # When resolving conflicts git commit -m "fix(sdk): resolve generation conflicts in payment model" ``` ## Troubleshooting tips ### Common patterns to avoid **Do not remove generated headers** ```typescript // ❌ Do not remove these // Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT. // @generated-id: a1b2c3d4e5f6 // ✅ Keep them for move detection to work ``` **Do not copy files with IDs** ```bash # ❌ Copying creates duplicate IDs cp src/models/user.ts src/models/user-v2.ts # ✅ Either move or create new file mv src/models/user.ts src/models/user-v2.ts # or create fresh without the @generated-id header ``` ### Recovery procedures **Reset a single file** ```bash # Remove custom changes from one file git checkout HEAD -- src/models/payment.ts # Re-run generation using the same pristine snapshot (no new snapshot is created) speakeasy run --skip-versioning ``` **Reset everything** ```bash # Disable custom code # Edit .speakeasy/gen.yaml: enabled: false # Remove all generated files find . -name "*.gen.*" -delete # Adjust pattern for the SDK # Regenerate fresh speakeasy run ``` **Fix "duplicate ID" warnings** 1. Find files with duplicate IDs 2. Remove `@generated-id` line from copied files 3. Let next generation assign new IDs ## Summary checklist ✓ Enable custom code before reorganizing files. Move detection is file-specific, not folder-specific ✓ Document customizations for team members ✓ Do not remove @generated-id headers ✓ Commit custom edits independently of generator changes for clarity # Custom code technical reference Source: https://speakeasy.com/docs/sdks/customize/code/custom-code/custom-code-reference import { Callout } from "@/mdx/components"; This reference describes the internal implementation of custom code. Understanding these details is not required to use the feature - this information is for debugging, troubleshooting, or satisfying technical curiosity. ## How the 3-way merge works Custom code uses a 3-way merge algorithm similar to Git merge. For each file, three versions are tracked: ```mermaid graph LR A[Base
Last pristine generation] --> D[3-Way Merge] B[Current
Modified version on disk] --> D C[New
Latest generation] --> D D --> E[Result
Merged output] ``` The merge process: 1. **Base**: The pristine version from the last generation (stored in Git objects) 2. **Current**: The version on disk (potentially with custom changes) 3. **New**: The newly generated version from Speakeasy If changes do not overlap, the merge completes automatically. When changes overlap, Speakeasy writes conflict markers to the file and stages the conflict in Git's index using three-stage entries (base, ours, theirs). This ensures `git status`, IDEs, and other Git tools recognize the conflict and prompt for resolution. ## File tracking and move detection ### Generated file headers Each generated file contains a tracking header: ```typescript // Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT. // @generated-id: a1b2c3d4e5f6 ``` The `@generated-id` is a deterministic hash based on the file's original path. This allows Speakeasy to detect when files are moved: 1. File generated at `src/models/user.ts` gets ID based on that path 2. File is moved to `src/entities/user.ts` 3. Next generation, Speakeasy scans for the ID and finds it at the new location 4. Updates are applied to the moved file ### Lockfile structure The `.speakeasy/gen.lock` file tracks generation state: ```json { "generationVersion": "2.500.0", "persistentEdits": { "generation_id": "abc-123", "pristine_commit_hash": "deadbeef123", "pristine_tree_hash": "cafebabe456" }, "trackedFiles": { "src/models/user.ts": { "id": "a1b2c3d4e5f6", "last_write_checksum": "sha1:7890abcdef", "pristine_git_object": "blobhash123", "moved_to": "src/entities/user.ts" } } } ``` Fields explained: - `generation_id`: Unique ID for this generation - `pristine_commit_hash`: Git commit storing pristine generated code - `pristine_tree_hash`: Git tree hash for no-op detection - `trackedFiles`: Per-file tracking information - `id`: File's generated ID for move detection - `last_write_checksum`: Checksum when last written (dirty detection) - `pristine_git_object`: Git blob hash of pristine version - `moved_to`: New location if file was moved ## Git integration details ### Object storage Custom code stores pristine generated code using Git's object database: 1. **Blob objects**: Each generated file is stored as a blob 2. **Tree objects**: Directory structure is stored as trees 3. **Commit objects**: Each generation creates a commit These objects live in `.git/objects` but are not referenced by any branch. ### Git refs Generation snapshots are stored at: ``` refs/speakeasy/gen/ ``` These refs: - Are not visible in `git branch` or GitHub - Keep objects reachable so `git gc` does not prune them - Can be fetched/pushed like any other ref ### Git commands used The Speakeasy CLI uses the following Git operations internally: | Operation | Git command | | --- | --- | | Store file content | `git hash-object -w --stdin` | | Create tree | `git write-tree` | | Create commit | `git commit-tree` | | Read pristine content | `git cat-file -p ` | | Check object exists | `git cat-file -e ` | | Stage conflict | `git update-index --index-info` | | Fetch snapshot | `git fetch origin refs/speakeasy/gen/` | | Push snapshot | `git push origin :refs/speakeasy/gen/` | When conflicts are detected, Speakeasy stages three versions of the file in Git's index (stages 1, 2, and 3 representing base, ours, and theirs). This is the same mechanism Git uses for merge conflicts, ensuring compatibility with standard Git conflict resolution tools. ## Edge cases and behavior ### File moves before enabling If files are moved before enabling custom code: - No `@generated-id` header exists - Move cannot be detected - File treated as deleted at old location, created at new location - History is broken for that file **Recommendation**: Enable custom code before reorganizing files. ### Duplicate IDs If a file is copied (not moved): - Both files have the same `@generated-id` - System detects duplicate and logs warning - Prefers the file at the expected (original) location - Other copies may not receive updates correctly ### Deleted files When a generated file is deleted: - Marked as `deleted: true` in lockfile - Speakeasy does not recreate it - To restore: remove the file entry from `trackedFiles` in gen.lock ### Binary files Binary files (images, JARs, etc.) are handled differently: - No `@generated-id` header (cannot add comments) - Tracked by path only (no move detection) - Replaced entirely on regeneration if changed Regeneration completely overwrites binary files. Any manual modifications to binary files are lost. ### CI mode detection Custom code runs in two modes: - **Interactive mode**: when stdin is a TTY and no explicit CI mode is set. Prompts like "Enable custom code?" are shown. - **CI mode**: when running non-interactively (no TTY) or when `mode: ci` is configured in the SDK generation action or CLI environment. Prompts are suppressed and conflicts cause a non-zero exit. In GitHub Actions, the `speakeasy-api/sdk-generation-action` automatically sets the appropriate mode. ## Troubleshooting ### Common issues #### "Failed to fetch pristine snapshot" This occurs when: - Remote repository is unreachable - CI does not have permission to fetch refs **Solution**: Ensure full clone in CI: ```yaml - uses: actions/checkout@v4 with: fetch-depth: 0 # Full clone, not shallow ``` #### "Duplicate generated ID detected" This means multiple files have the same `@generated-id`: - Check if files were copied instead of moved - Remove duplicate headers from copied files - Let Speakeasy assign new IDs on next generation #### "Cannot resolve conflicts automatically" Manual changes and Speakeasy updates modify the same lines: 1. Open the conflicted files 2. Resolve conflicts manually (keep desired code) 3. Run `speakeasy run --skip-versioning` 4. Commit the resolution The `--skip-versioning` flag tells Speakeasy to reuse the existing pristine snapshot (the "Base" in the 3-way merge) instead of creating a new snapshot from the conflicted state. This keeps the merge inputs stable while resolving conflicts. ### Resetting to pristine state #### Full reset (low-level) To discard all customizations and also reset custom code internal tracking: 1. Delete all generated files 2. (Optional, advanced) Remove the `persistentEdits` section from `.speakeasy/gen.lock` 3. Run `speakeasy run` to generate fresh files and create a new snapshot For a higher-level reset workflow that does not require editing internal tracking files, see the "How to reset to pristine generated code" FAQ in the [main custom code guide](/docs/sdks/customize/code/custom-code/custom-code). To temporarily disable without losing configuration: ```yaml persistentEdits: enabled: false # or "never" to prevent prompts ``` ### Inspecting Git objects To debug custom code internals: ```bash # View stored refs git show-ref | grep refs/speakeasy/gen/ # Inspect a specific generation git log --oneline refs/speakeasy/gen/ # See pristine version of a file git show # Check if object exists locally git cat-file -e && echo "exists" || echo "missing" ``` ## Performance considerations ### Repository size Generation snapshots use a commit history (each new generation commit has the previous as its parent), enabling Git delta compression. This keeps storage efficient: - Git pack files compress content efficiently using deltas between generations - Identical files share storage (deduplication) - Only the latest generation needs to be fetched - Old generations can be pruned if needed ### Initial scan On first run with existing modifications: - Speakeasy scans all files for `@generated-id` headers - This can be slow for very large SDKs (1000+ files) - Subsequent runs use cached information from lockfile ### Merge performance The 3-way merge is performed per-file: - Clean files (no changes) are fast - just overwrite - Modified files require diff computation - Conflicts are rare in practice ## Security considerations ### Git permissions Custom code requires: - Read access to `.git/objects` - Ability to create Git objects locally - Optional: push access to `refs/speakeasy/gen/*` (soft failure if unavailable) ### Remote operations - Fetch: Attempts to get latest pristine snapshot - Push: Attempts to backup new snapshot - Both operations are "fire and forget"; failures do not block generation ### No branch access Custom code never: - Creates or modifies branches - Changes commit history - Modifies the working branch - Requires access to source branches ## Language-specific behavior ### Comment syntax Different languages use different comment styles for the `@generated-id` header: | Language | Comment style | | --- | --- | | Go, Java, JavaScript, TypeScript, C# | `// @generated-id: abc123` | | Python, Ruby, Shell | `# @generated-id: abc123` | | HTML, XML | `` | | CSS | `/* @generated-id: abc123 */` | ### Files without comment support Some file types do not support comments: - JSON files - Binary files - Some configuration formats These files: - Cannot have `@generated-id` headers - Are tracked by path only - Do not support move detection # Custom code Source: https://speakeasy.com/docs/sdks/customize/code/custom-code/custom-code import { Callout, CodeWithTabs } from "@/mdx/components"; Custom code allows changes anywhere in generated SDKs. Speakeasy automatically preserves those changes across regenerations. Unlike [custom code regions](/docs/sdks/customize/code/code-regions/overview), which require predefined areas for customization, custom code provides complete flexibility to modify any generated file. ## How it works Speakeasy preserves manual edits by performing a 3-way merge during generation. This process combines the pristine generated code from the previous generation, the current code on disk, and the newly generated code. If manual changes and generated updates touch the same lines, standard Git-style conflict markers appear for resolution in the editor. Custom code requires a Git repository. The feature uses Git under the hood to track and merge changes, but direct Git interaction is not necessary. ## Enabling custom code ### For new SDKs Add the configuration to `gen.yaml`: ```yaml filename=".speakeasy/gen.yaml" configVersion: 2.0.0 generation: sdkClassName: MySDK # ... other configuration persistentEdits: # Enables custom code preservation enabled: true ``` ### For existing SDKs When modifying a generated file and running Speakeasy, a prompt appears showing a diff of the changes: ```ansi ┃ Changes detected in generated SDK files ┃ The following changes were detected in generated SDK files: ┃ M package.json (+2/-1) ┃ --- generated ┃ +++ current ┃ @@ -22,7 +22,8 @@ ┃ "scripts": { ┃ "lint": "eslint --cache --max-warnings=0 src", ┃ "build": "tshy", ┃ - "prepublishOnly": "npm run build" ┃ + "prepublishOnly": "npm run build", ┃ + "test:integration": "node ./scripts/integration.js" ┃ }, ┃ ... (2 more lines) ┃ ┃ Would you like to enable custom code preservation? ┃ Yes - Enable custom code ┃ > No - Continue without preserving changes ┃ Don't ask again ``` Select **"Yes - Enable custom code"** to preserve changes. Speakeasy updates `gen.yaml` automatically. ## When to use custom code Custom code is appropriate when: - Adding utility methods or business logic to generated models - Customizing SDK initialization or configuration - Integrating with internal systems or libraries - Making changes that do not fit into predefined extension points For simpler customizations, consider: - [SDK hooks](/docs/sdks/customize/code/sdk-hooks) for lifecycle customization - [Custom code regions](/docs/sdks/customize/code/code-regions/overview) for predefined extension areas (Enterprise only) - OpenAPI extensions for generation-time customization ## Common use cases ### Adding utility methods Add helper methods directly to generated model classes: ```typescript export class Payment { id: string; amount: number; currency: string; status: PaymentStatus; // Custom utility method toInvoiceItem(): InvoiceItem { return { description: `Payment ${this.id}`, amount: this.amount, currency: this.currency, }; } needsAction(): boolean { return this.status === PaymentStatus.RequiresAction || this.status === PaymentStatus.RequiresConfirmation; } } ``` ### Modifying configuration files Add custom dependencies or scripts to package configuration: ### Extending SDK initialization Add custom authentication providers or configuration: ```typescript import { AWSAuth } from "./custom/aws-auth"; import { MetricsCollector } from "./custom/metrics"; export class MySDK { private client: HTTPClient; private awsAuth?: AWSAuth; private metrics?: MetricsCollector; constructor(config: SDKConfig) { this.client = new HTTPClient(config); // Custom initialization logic if (config.awsAuth) { this.awsAuth = new AWSAuth(config.awsAuth); this.client.interceptors.request.use( this.awsAuth.signRequest.bind(this.awsAuth) ); } if (config.enableMetrics) { this.metrics = new MetricsCollector(config.metricsEndpoint); this.client.interceptors.response.use( this.metrics.recordResponse.bind(this.metrics) ); } } } ``` ## Handling conflicts When manual changes and Speakeasy updates modify the same lines, Speakeasy detects the conflict, adds conflict markers to the file, and stages the conflict in the Git index. This ensures that IDEs, `git status`, and other Git tools recognize the conflict and prompt for resolution: ```bash Merge conflicts detected 1 file(s) have conflicts that must be resolved manually: both modified: package.json To resolve: 1. Open each file and resolve the conflict markers (<<<<<<, ======, >>>>>>) 2. Remove the conflict markers after choosing the correct code 3. Run: speakeasy run --skip-versioning ``` For example, when the SDK version uses a customized prerelease tag and Speakeasy bumps the version during generation: ```diff filename="package.json" { "name": "petstore", <<<<<<< Current (local changes) "version": "0.0.2-prerelease", ======= "version": "0.0.3", >>>>>>> New (Generated by Speakeasy) "dependencies": { ... } } ``` To resolve conflicts: 1. Edit the file to keep the desired code (in this case, decide on the correct version) 2. Remove the conflict markers (`<<<<<<<`, `=======`, `>>>>>>>`) 3. Run `speakeasy run --skip-versioning` to finalize the merge and update internal tracking to accept the resolution 4. Commit the resolved changes ## Working in CI/CD Custom code works seamlessly in CI/CD environments. The feature runs non-interactively in CI: - No prompts appear - Conflicts cause the generation to fail with a clear error message - The CI job exits with a non-zero code if conflicts occur The Speakeasy GitHub Action automatically rolls back to the last working generator version if conflicts or other issues occur. This ensures SDK generation remains stable while conflicts are resolved. ### Handling conflicts in CI If conflicts occur during CI generation: - The CI job fails with an error message listing conflicted files - Pull the changes locally - Run `speakeasy run` to reproduce the conflicts - Resolve conflicts manually - Commit and push the resolution - CI succeeds on the next run ## Configuration options ```yaml filename=".speakeasy/gen.yaml" persistentEdits: # Enable or disable custom code enabled: true ``` ### Disabling custom code To disable custom code without losing changes: ```yaml filename=".speakeasy/gen.yaml" persistentEdits: enabled: false ``` To prevent prompts entirely: ```yaml filename=".speakeasy/gen.yaml" persistentEdits: enabled: never ``` ## Frequently asked questions ### Editing files in the GitHub web UI Changes made in the GitHub web UI or any Git-based tool are treated like any other manual change. When the repository is cloned locally and Speakeasy runs in that clone, edits are preserved. ### Removing generated headers Do not remove generated headers (like `// @generated-id: abc123`) when planning to move files. These headers contain tracking information that helps Speakeasy detect file moves. If headers are removed: - Custom code still works for files that stay in the same location - File move detection does not work - moved files are treated as deleted and recreated ### Using custom code with custom code regions Both features can be used together. Custom code regions provide predefined safe areas for customization, while custom code allows changes anywhere. Choose the approach that best fits the use case. ### Git operations performed See the [technical reference](/docs/sdks/customize/code/custom-code/custom-code-reference#git-integration-details) for details on Git integration. ### Resetting to pristine generated code To discard all custom changes and get fresh generated code: 1. Disable custom code in `gen.yaml` 2. Delete the modified files 3. Run `speakeasy run` to regenerate fresh files 4. Re-enable custom code if desired For resetting a single file while keeping other customizations, see the [best practices guide](/docs/sdks/customize/code/custom-code/custom-code-best-practices#recovery-procedures). ## Next steps - Review the [technical reference](/docs/sdks/customize/code/custom-code/custom-code-reference) for implementation details - Check out [best practices](/docs/sdks/customize/code/custom-code/custom-code-best-practices) for team workflows - Learn about [custom code regions](/docs/sdks/customize/code/code-regions/overview) for predefined customization areas # Ignoring files Source: https://speakeasy.com/docs/sdks/customize/code/custom-code/genignore import { Callout } from "@/mdx/components"; **Advanced technique:** Ignoring files is an advanced feature that requires additional maintenance effort and can introduce complexity for SDKs. Ignoring files removes matched files from the generation process and grants full ownership of those files. This approach can address bugs, add features, or implement workarounds that are not supported by the source API or generator configuration. In Speakeasy, this includes modifying generated SDK code, documentation, or examples. For most persistent changes to generated SDKs, the [Custom code](/docs/sdks/customize/code/custom-code/custom-code) feature is recommended. Custom code uses a three-way merge to preserve edits across generations. Ignoring files increases maintenance effort and can introduce inconsistencies, failures, and discrepancies within SDKs. The trade-offs should be evaluated carefully, as this approach can add significant overhead. ## Recommended use cases Ignoring files can be beneficial in low-impact scenarios such as: - Customizing usage snippets or example code. - Modifying generated documentation. - Implementing additional business logic that the API does not handle. When possible, avoid ignoring core generated code or package dependencies, because these changes can introduce issues during SDK generation or result in failures. ## Mark files with `.genignore` To ignore files, add a `.genignore` file to the project. This file behaves similarly to `.gitignore` but signals that the files it matches are managed manually rather than by the SDK generator. Rules in `.genignore` follow this syntax: - Blank lines match nothing and improve readability. - Lines starting with `#` are comments. Escape `#` with a backslash (`\#`) to match a file starting with `#`. - Trailing spaces are ignored unless escaped with `\`, which preserves spaces. - Lines starting with `!` denote negative matches (files matching that line will not be ignored). - Lines ending with `/` match directories only. - Each non-empty, non-comment line is interpreted as a glob pattern (for example, `*.go` matches all `.go` files). - Wildcards (`*`, `?`) do not match directory separators. - `**` matches across multiple directory levels. - Lines starting with `/` match paths relative to the directory that contains the `.genignore` file. Once a file is included in `.genignore`, the generator does not modify that file. Files generated by Speakeasy include the following header: ```go // Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT. ``` To mark a file as manually maintained, update the header to: ```go // Code originally generated by Speakeasy (https://speakeasy.com). ``` This header distinguishes manually maintained files from files that continue to be managed by the generator. ## Caveats Ignoring files can introduce the following issues: - **Duplicated code:** Changes in naming conventions during generation can result in duplicate symbols between ignored files and generated code. - **Missing code:** Internal generator changes can replace or rename symbols, causing ignored files to break. - **Dead code:** Ignored files may become obsolete if they are no longer referenced by the SDK, resulting in unused code. Each SDK generation can introduce these or other maintenance challenges. Ignore files only when necessary. ## Recovering from issues The following workflow can help resolve issues caused by ignored files: - Comment out the affected lines in `.genignore`. - Regenerate the SDK using the Speakeasy CLI or GitHub Action. - Compare changes between the regenerated SDK and the ignored files using `git diff`. - Update the ignored files as required, then uncomment or restore the relevant lines in `.genignore`. - Commit changes to maintain synchronization between ignored files and the generated SDK. # Custom Code With SDK Hooks Source: https://speakeasy.com/docs/sdks/customize/code/sdk-hooks import { Callout, CodeWithTabs, Table } from "@/mdx/components"; SDK Hooks are available for [Business and Enterprise users](/pricing) only. SDK Hooks enable custom logic to be added to SDK functions and request lifecycles across supported SDKs. These hooks allow for transformations, tracing, logging, validation, and error handling during different stages of the SDK's lifecycle. Hooks can be applied to the following lifecycle events: - **On SDK initialization:** Modify the SDK configuration, base server URL, wrap or override the HTTP client, add tracing, inject global headers, and manage authentication. - **Before request:** Cancel an outgoing request, transform the request contents, or add tracing. Access to SDK configuration and operation context. - **After success:** When a successful response is received, add tracing and logging, validate the response, return an error, or transform the raw response before deserialization. Access to SDK configuration and operation context. - **After error:** On connection errors or unsuccessful responses, add tracing and logging or transform the returned error. Access to SDK configuration and operation context. ## Hook Context All hooks (except SDK initialization) receive a `HookContext` object that provides access to: - **SDK Configuration:** The complete SDK configuration object, allowing hooks to access custom settings, authentication details, and other configuration parameters. - **Base URL:** The base URL being used for the request. - **Operation ID:** The unique identifier for the API operation being called. - **OAuth2 Scopes:** The OAuth2 scopes required for the operation (if applicable). - **Security Source:** The security configuration or source for the operation. - **Retry Configuration:** The retry settings for the operation. ## SDK Configuration Access SDK configuration access in hooks is controlled by the `sdkHooksConfigAccess` feature flag in the `generation` section of your `gen.yaml` configuration file. The `sdkHooksConfigAccess` feature flag determines whether hooks have access to the complete SDK configuration object: - **`sdkHooksConfigAccess: true`** (default for newly generated SDKs): Hooks receive full access to the SDK configuration through the `HookContext` object, and the SDK initialization hook receives the complete configuration object as a parameter. - **`sdkHooksConfigAccess: false`** (default for SDKs generated before May 2025): Hooks have limited access to SDK configuration, and the SDK initialization hook has a different signature that doesn't include the configuration parameter. ### Version Compatibility - **New SDKs (May 2025 and later)**: The `sdkHooksConfigAccess` flag defaults to `true`, providing full configuration access. - **Existing SDKs (before May 2025)**: The flag defaults to `false` to maintain backward compatibility. You can manually set it to `true` in your `gen.yaml` file to enable full configuration access. When `sdkHooksConfigAccess` is set to `false`, the SDK initialization hook will have a different signature that doesn't receive the configuration object as a parameter, limiting the customization options available during SDK initialization. To enable full SDK configuration access in existing SDKs, add `sdkHooksConfigAccess: true` under the `generation` section in your `gen.yaml` file. ## Add a Hook Hooks are supported in SDKs generated with the latest Speakeasy CLI. Each supported language includes a hooks directory in the generated code:
### Steps to Add a Hook 1. **Create a hook implementation.** Add the custom hook implementation in a new file inside the `hooks` directory. The generator won't override files added to this directory. 2. **Locate the registration file.** Find the appropriate registration file for the language:
3. **Instantiate and register the hook.** In the registration file, find the method `initHooks/init_hooks/initialize/InitHooks`. This method includes a hooks parameter, allowing hooks to be registered for various lifecycle events. Instantiate the hook here and register it for the appropriate event. { public static class HookRegistration { public static void InitHooks(IHooks hooks) { var exampleHook = new ExampleHook(); hooks.RegisterBeforeRequestHook(exampleHook); } } }`, }, { label: "PHP", language: "php", code: `; class HookRegistration { /** * @param Hooks $hooks */ public static function initHooks(Hooks $hooks): void { $exampleHook = new ExampleHook(); $hooks->registerBeforeRequestHook($exampleHook); } }`, }, { label: "Ruby", language: "ruby", code: `def self.init_hooks(hooks) example_hook = ExampleHook.new hooks.register_before_request_hook example_hook end`, } ]} /> The registration file is generated once and will not be overwritten. After the initial generation, you have full control and ownership of it. Here are example hook implementations for each of the lifecycle events across different languages: SDKConfiguration: # modify the SDK configuration, base_url, or wrap the client used by the SDK here and return the # updated configuration # Access config.base_url, config.client, and other configuration options return config def before_request( self, hook_ctx: BeforeRequestContext, request: httpx.Request ) -> Union[httpx.Request, Exception]: # Access SDK configuration: hook_ctx.config # Access operation details: hook_ctx.operation_id, hook_ctx.base_url # modify the request object before it is sent, such as adding headers or query # parameters, or raise an exception to stop the request return request def after_success( self, hook_ctx: AfterSuccessContext, response: httpx.Response ) -> Union[httpx.Response, Exception]: # Access SDK configuration: hook_ctx.config # Access operation details: hook_ctx.operation_id, hook_ctx.base_url # modify the response object before deserialization or raise an exception to stop # the response from being returned return response def after_error( self, hook_ctx: AfterErrorContext, response: Optional[httpx.Response], error: Optional[Exception], ) -> Union[Tuple[Optional[httpx.Response], Optional[Exception]], Exception]: # Access SDK configuration: hook_ctx.config # Access operation details: hook_ctx.operation_id, hook_ctx.base_url # modify the response before it is deserialized as a custom error or the error # object before it is returned or raise an exception to stop processing of other # error hooks and return early return response, error`, }, { label: "Java", language: "java", code: `package dev.speakeasyapi.speakeasy.hooks; import dev.speakeasyapi.speakeasy.utils.Utils; import dev.speakeasyapi.speakeasy.utils.Hook.AfterError; import dev.speakeasyapi.speakeasy.utils.Hook.AfterErrorContext; import dev.speakeasyapi.speakeasy.utils.Hook.AfterSuccess; import dev.speakeasyapi.speakeasy.utils.Hook.AfterSuccessContext; import dev.speakeasyapi.speakeasy.utils.Hook.BeforeRequest; import dev.speakeasyapi.speakeasy.utils.Hook.BeforeRequestContext; import dev.speakeasyapi.speakeasy.utils.Hook.SdkInit; import dev.speakeasyapi.speakeasy.SDKConfiguration; import java.io.InputStream; import java.net.http.HttpRequest; import java.net.http.HttpResponse; import java.util.Optional; final class ExampleHook implements BeforeRequest, AfterError, AfterSuccess, SdkInit { @Override public SDKConfiguration sdkInit(SDKConfiguration config) { // modify the SDK configuration, baseURL, or wrap the client used by the SDK here and return the updated config // Access config properties and modify as needed return config; } @Override public HttpRequest beforeRequest(BeforeRequestContext context, HttpRequest request) throws Exception { // Access SDK configuration: context.sdkConfiguration() // Access operation details: context.operationId(), context.baseUrl() // modify the request object before it is sent, such as adding headers or query parameters // or throw an error to stop the request from being sent // Note that HttpRequest is immutable. With JDK 16 and later you can use // \`HttpRequest.newBuilder(HttpRequest, BiPredicate)\` to copy the request // and modify it (the predicate is for filtering headers). If that method is not // available then use \`Helpers.copy\` in the generated \`utils\` package. return request; } @Override public HttpResponse afterSuccess(AfterSuccessContext context, HttpResponse response) throws Exception { // Access SDK configuration: context.sdkConfiguration() // Access operation details: context.operationId(), context.baseUrl() // modify the response object before deserialization or throw an exception to stop the // response from being deserialized return response; } @Override public HttpResponse afterError(AfterErrorContext context, Optional> response, Optional error) throws Exception { // Access SDK configuration: context.sdkConfiguration() // Access operation details: context.operationId(), context.baseUrl() // modify the response before it is deserialized as a custom error or the exception // object before it is thrown or throw a FailEarlyException to stop processing of // other error hooks and return early return response.orElse(null); } }`, }, { label: "C#", language: "csharp", code: `namespace Speakeasy.Hooks { using Speakeasy.Utils; using Speakeasy.Models.Components; public class ExampleHook : ISDKInitHook, IBeforeRequestHook, IAfterSuccessHook, IAfterErrorHook { public SDKConfig SDKInit(SDKConfig config) { // modify the SDK configuration, baseURL, or wrap the client used by the SDK here and return the updated config // Access config.BaseURL, config.Client, and other configuration options return config; } public async Task BeforeRequestAsync(BeforeRequestContext hookCtx, HttpRequestMessage request) { // Access SDK configuration: hookCtx.SDKConfiguration // Access operation details: hookCtx.OperationID, hookCtx.BaseURL // modify the request object before it is sent, such as adding headers or query parameters, or throw an exception to stop the request from being sent return request; } public async Task AfterSuccessAsync(AfterSuccessContext hookCtx, HttpResponseMessage response) { // Access SDK configuration: hookCtx.SDKConfiguration // Access operation details: hookCtx.OperationID, hookCtx.BaseURL // modify the response object before deserialization or throw an exception to stop the response from being returned return response; } public async Task<(HttpResponseMessage?, Exception?)> AfterErrorAsync(AfterErrorContext hookCtx, HttpResponseMessage? response, Exception error) { // Access SDK configuration: hookCtx.SDKConfiguration // Access operation details: hookCtx.OperationID, hookCtx.BaseURL // modify the response before it is deserialized as a custom error // return (response, null); // OR modify the exception object before it is thrown // return (null, error); // OR abort the processing of subsequent AfterError hooks // throw new FailEarlyException("return early", error); // response and error cannot both be null return (response, error); } } }`, }, { label: "PHP", language: "php", code: `namespace Speakeasy\\Hooks; class ExampleHook implements AfterErrorHook, AfterSuccessHook, BeforeRequestHook, SDKInitHook { public function sdkInit(SDKConfiguration $config): SDKConfiguration { // modify the SDK configuration, baseURL, or wrap the client used by the SDK here and return the updated config // Access config properties and modify as needed return $config; } public function beforeRequest(BeforeRequestContext $context, RequestInterface $request): RequestInterface { // Access SDK configuration: $context->config // Access operation details: $context->operationID, $context->baseURL // modify the request object before it is sent, such as adding headers or query parameters, or throw an exception to stop the request from being sent return $request; } public function afterSuccess(AfterSuccessContext $context, ResponseInterface $response): ResponseInterface { // Access SDK configuration: $context->config // Access operation details: $context->operationID, $context->baseURL // modify the response object before deserialization or throw an exception to stop the response from being returned return $response; } public function afterError(AfterErrorContext $context, ?ResponseInterface $response, ?\\Throwable $exception): ErrorResponseContext { // Access SDK configuration: $context->config // Access operation details: $context->operationID, $context->baseURL // modify the response before it is deserialized as a custom error // return new ErrorResponseContext($response, null); // OR modify the exception object before it is thrown // return new ErrorResponseContext(null, $exception); // OR abort the processing of subsequent AfterError hooks // throw new FailEarlyException("return early", $exception); // response and error cannot both be null return new ErrorResponseContext($response, $exception); } }`, }, { label: "Ruby", language: "ruby", code: `# typed: true # frozen_string_literal: true require_relative './types' require 'sorbet-runtime' module ExampleSDK module SDKHooks class ExampleHook extend T::Sig include AbstractSDKInitHook include AbstractBeforeRequestHook include AbstractAfterSuccessHook include AbstractAfterErrorHook sig do override.params( config: SDKConfiguration ).returns(SDKConfiguration) end def sdk_init(config:) # modify the SDK configuration, base_url, or wrap the client used by the SDK here and return # the updated configuration # Access config properties and modify as needed config end sig do override.params( hook_ctx: BeforeRequestHookContext, request: Faraday::Request ).returns(Faraday::Request) end def before_request(hook_ctx:, request:) # Access SDK configuration: hook_ctx.config # Access operation details: hook_ctx.operation_id, hook_ctx.base_url # modify the request object before it is sent, such as adding headers or # query parameters, or raise an exception to stop the request request end sig do override.params( hook_ctx: AfterSuccessHookContext, response: Faraday::Response ).returns(Faraday::Response) end def after_success(hook_ctx:, response:) # Access SDK configuration: hook_ctx.config # Access operation details: hook_ctx.operation_id, hook_ctx.base_url # modify the response object before deserialization or raise an # exception to stop the response from being returned response end sig do override.params( error: T.nilable(StandardError), hook_ctx: AfterErrorHookContext, response: T.nilable(Faraday::Response) ).returns(T.nilable(Faraday::Response)) end def after_error(error:, hook_ctx:, response:) # Access SDK configuration: hook_ctx.config # Access operation details: hook_ctx.operation_id, hook_ctx.base_url # modify the response before it is deserialized or raise an exception to # stop processing of other error hooks and return early response end end end end`, } ]} /> ## Async Hooks (Python) Python SDKs support async hooks for non-blocking I/O in async methods. Enable with `useAsyncHooks: true` in `gen.yaml`: ```yaml python: useAsyncHooks: true ``` **Key points:** - Async hooks use `async/await` syntax and are registered in `asyncregistration.py` - Existing sync hooks automatically work in async contexts (adapted via `asyncio.to_thread()`) - Native async hooks provide better performance than adapted sync hooks for I/O-heavy operations For complete documentation including implementation examples, migration paths, and adapter usage, see [Async Hooks for Python](/docs/sdks/customize/python/async-hooks). ## Adding Dependencies To add dependencies needed for SDK hooks, configure the `additionalDependencies` section in the `gen.yaml` file.

=6.0.0" - "flake8>=3.9.0" main: - "requests>=2.25.0" - "pydantic>=1.8.0" `, }, { label: "Java", language: "yaml", code: `java: additionalDependencies: # Pass an array of \`scope:groupId:artifactId:version\` strings, for example, \`implementation:com.fasterxml.jackson.core:jackson-databind:2.16.2\`. - implementation:com.fasterxml.jackson.core:jackson-databind:2.16.0 - api:org.apache.commons:commons-compress:1.26.1 `, }, { label: "C#", language: "yaml", code: `csharp: additionalDependencies: - package: RestSharp version: 106.12.0 includeAssets: all privateAssets: contentFiles; build; native; analyzers; buildTransitive excludeAssets: none `, }, { label: "PHP", language: "yaml", code: `php: additionalDependencies: require: "firebase/php-jwt": "^6.10" require-dev: "roave/security-advisories": "dev-latest" `, }, { label: "Ruby", language: "yaml", code: `configVersion: 2.0.0 ruby: additionalDependencies: runtime: "faraday-net_http_persistent": "~>2.3" development: "rubocop-faker": "1.3.0" `, }, ]} /> # Additional properties Source: https://speakeasy.com/docs/sdks/customize/data-model/additionalproperties When OpenAPI schemas use the [`additionalProperties`](https://json-schema.org/understanding-json-schema/reference/object#additional-properties) keyword, Speakeasy generates SDKs that handle these flexible object types automatically. ## Using `additionalProperties` in SDKs **Important**: `additionalProperties` is an internal implementation detail. Pass objects with all properties directly - the SDK handles serialization and validation automatically. ## The Speakeasy approach Speakeasy sets `additionalProperties` to `false` by default unless explicitly defined otherwise. This encourages fully-typed objects with clear documentation. Most developers prefer closed objects by default, but setting `additionalProperties: false` in every schema would be tedious. Most backend frameworks that generate OpenAPI schemas don't add `additionalProperties: false`, even when that's the intended behavior. ## Using `additionalProperties: true` Set `additionalProperties: true` to accept arbitrary fields beyond the defined properties. This is useful for accepting unpredictable fields or allowing future extensions without schema updates. ```yaml filename="openapi.yaml" components: schemas: FlexibleObject: type: object properties: title: type: string description: type: string additionalProperties: true ``` ### Generated type structure ```typescript export type FlexibleObject = { title: string; description: string; additionalProperties: Record; // Internal field - do not interact directly } ``` ### Usage ```typescript await sdk.items.create({ title: "My Item", description: "A flexible item", customField: "custom value", anotherField: 123, metadata: { key: "value" } }); ``` ## Using typed `additionalProperties` Constrain additional properties to a specific type: ```yaml filename="openapi.yaml" components: schemas: StringOnlyObject: type: object properties: title: type: string description: type: string additionalProperties: type: string ``` # The `allOf` keyword Source: https://speakeasy.com/docs/sdks/customize/data-model/allof-schemas import { Callout } from "@/mdx/components"; The OpenAPI `allOf` keyword enables schema composition by merging multiple schema definitions into a single schema. Speakeasy provides two strategies for handling `allOf` merging: shallow merge and deep merge. ## Merge strategies The `schemas.allOfMergeStrategy` configuration option in `gen.yaml` controls how Speakeasy merges `allOf` schemas. This setting is located under the `generation` section of the configuration file. ```yaml generation: schemas: allOfMergeStrategy: deepMerge # or shallowMerge ``` ### Available strategies **`deepMerge` (default for new SDKs)**: Recursively merges nested properties within objects, preserving properties from all schemas in the `allOf` array. **`shallowMerge` (legacy behavior)**: Replaces entire property blocks when merging, which can result in lost properties from earlier schemas. New SDKs default to `deepMerge`. Existing SDKs continue to use `shallowMerge` unless explicitly changed. Switching from `shallowMerge` to `deepMerge` may be a breaking change for existing SDKs. ## Deep merge behavior With `deepMerge` enabled, nested properties are recursively combined rather than replaced. This is particularly useful when extending base schemas with additional nested properties. Consider this OpenAPI definition: ```yaml components: schemas: Base: type: object properties: id: type: string metadata: type: object properties: createdAt: type: string updatedAt: type: string Extended: allOf: - $ref: '#/components/schemas/Base' - type: object properties: name: type: string metadata: type: object properties: deletedAt: type: string ``` With `deepMerge`, the resulting merged schema preserves all metadata properties: ```yaml components: schemas: Extended: type: object properties: id: type: string metadata: type: object properties: createdAt: type: string updatedAt: type: string deletedAt: type: string name: type: string ``` The `metadata` object includes all three timestamp properties: `createdAt`, `updatedAt`, and `deletedAt`. ## Shallow merge behavior With `shallowMerge`, entire property blocks are replaced during merging. When the same property name appears in multiple schemas, only the last occurrence is retained. Using the same example from above with `shallowMerge`: ```yaml components: schemas: Extended: type: object properties: id: type: string metadata: type: object properties: deletedAt: type: string name: type: string ``` The `metadata` properties `createdAt` and `updatedAt` from the `Base` schema are lost because the entire `metadata` properties block was replaced by the one in the `Extended` schema. ## Configuration To configure the merge strategy, add the `schemas.allOfMergeStrategy` option to the `generation` section of the `gen.yaml` file: ```yaml generation: schemas: allOfMergeStrategy: deepMerge ``` ### Switching strategies Changing from `shallowMerge` to `deepMerge` may affect the generated SDK's type definitions and could be a breaking change. Review the impact on existing generated code before making this change in production SDKs. To explicitly use the legacy behavior: ```yaml generation: schemas: allOfMergeStrategy: shallowMerge ``` ## Use cases ### Deep merge use cases Deep merge is beneficial when: - Extending base schemas with additional nested properties - Combining request and response schemas with shared metadata objects - Working with APIs that use `allOf` to compose complex nested structures - Maintaining all properties across schema inheritance hierarchies ### Shallow merge use cases Shallow merge may be appropriate when: - Maintaining backward compatibility with existing SDKs - Intentionally replacing entire nested objects from base schemas - Working with simple schema compositions without nested property conflicts ## Advanced example This pattern is common in APIs that separate shared components from operation-specific requirements and examples: ```yaml paths: /clusters: post: operationId: createCluster requestBody: content: application/json: schema: allOf: - $ref: '#/components/schemas/Cluster' - type: object required: - spec properties: spec: type: object required: - displayName - availability - type: object properties: spec: type: object properties: environment: example: { id: 'env-00000' } network: example: { id: 'n-00000' } ``` With `deepMerge`, the final `spec` object combines the required fields from the second schema with the examples from the third schema, while preserving all properties from the referenced `Cluster` component. With `shallowMerge`, the `spec` properties from the second schema would be lost entirely, replaced by only the example properties from the third schema. # Complex numbers Source: https://speakeasy.com/docs/sdks/customize/data-model/complex-numbers OpenAPI does not provide support natively for complex numbers. The highest precision integer type is an `integer` with an `int64` format, while the highest precision decimal value in the spec is a type `number` with a `double` format. To support arbitrary precision numbers, Speakeasy introduces two new formats for use in OpenAPI documents: `bigint`, which represents arbitrary precision integers, and `decimal`, which represents arbitrary precision decimal numbers. When these formats are used, the generated SDK will use the language-appropriate types to allow natively interacting with them. ## Preserve precision when serializing Generated SDKs treat `bigint` and `decimal` values as arbitrary precision and ensure their precision is maintained. During serialization, however, the value will be cast into the `type` of the field, which may result in a loss of precision. To prevent this, avoid using a numeric `type` in the OpenAPI document, and rather use the `string` type with a `bigint` or `decimal` format. This ensures that the value is serialized as a string, preserving its full precision, subject to the typical limitations of arbitrary precision decimal values in the language of choice. # Customize enums Source: https://speakeasy.com/docs/sdks/customize/data-model/enums import { Callout, CodeWithTabs } from "@/mdx/components"; ## Enum value naming ### Basic conversion Enum values are named according to their values, with adjustments made to form valid identifiers: - Invalid characters are removed. - Values are converted to fit the case style of the target programming language. - Special characters (for example, `+`, `-`, and `.`) are converted to words (like `Plus`, `Minus`, and `Dot`). ### Name conflicts If naming conflicts arise after sanitization, deduplication is attempted by modifying case styles or adding suffixes. For example, given the following schema: ```yaml schema: type: string enum: - foo - Foo - FOO ``` Resulting enum values will be `FOO_LOWER`, `FOO_MIXED`, and `FOO_UPPER`. If unique names cannot be resolved, a validation error will prompt you to resolve conflicts, potentially using the `x-speakeasy-enums` extension. The recommended approach is to use the map format, which prevents length mismatch errors: ```yaml schema: type: integer enum: - 1 - 2 - 3 x-speakeasy-enums: 1: NOT_STARTED 2: IN_PROGRESS 3: COMPLETE ``` Alternatively, you can use the array format, but ensure the order in the enum array corresponds exactly to the custom names in the `x-speakeasy-enums` array: ```yaml schema: type: integer enum: - 1 - 2 - 3 x-speakeasy-enums: - NOT_STARTED - IN_PROGRESS - COMPLETE ``` ## Enum class naming Use the `x-speakeasy-name-override` attribute to customize enum class names: ```yaml Enum: x-speakeasy-name-override: example_override type: string enum: - foo - FOO ``` This schema will produce: ```python class ExampleOverride(str, Enum): FOO_LOWER = 'foo' FOO_UPPER = 'FOO' ``` ### Name conflict considerations Some cases (like open enums) may pose unique name resolutions challenges, particularly when similar names occur in the schema. In name conflict cases, the parent schema is given the original name, while the child schema's name is concatenated with the parent's name. For example, the following schema: ```yaml enum_field: oneOf: - type: string - type: string enum: - foo - FOO x-speakeasy-name-override: enum_field ``` Results in: ```python class EnumFieldEnumField(str, Enum): FOO_LOWER = 'value' FOO_UPPER = 'value' ``` To avoid naming conflicts, additional overrides may be necessary. For example: ```yaml enum_field: x-speakeasy-name-override: enum_field_parent oneOf: - type: string - type: string enum: - foo - Foo x-speakeasy-name-override: enum_field ``` This will result in: ```python class EnumField(str, Enum): FOO_LOWER = 'value' FOO_UPPER = 'value' ``` ## Open vs closed enums This feature is currently supported in Go, Python, TypeScript, Java and C# SDKs. By default, enums defined in OpenAPI are considered closed during code generation. This means that introducing a new member to an enum can become a breaking change for consumers of older versions of the SDK. Sometimes, this is desirable because particular enums can be rigidly defined and not changing in the foreseeable future (country codes might be a good example of this). However, in some cases, you might want to make room for future iteration and the introduction of new enum members. This is where open enums can help. Use the `x-speakeasy-unknown-values` extension to mark an enum as open: ```yaml components: schemas: BackgroundColor: type: string x-speakeasy-unknown-values: allow enum: - red - green - blue ``` When an SDK is generated with this type, the API is able to send values beyond what is specified in the enum and these will be captured and returned to the user in a type-safe manner. Here's how the `BackgroundColor` model translates to different languages: ;`, }, { label: "Go", language: "go", code: `type BackgroundColor string const ( BackgroundColorRed BackgroundColor = "red" BackgroundColorGreen BackgroundColor = "green" BackgroundColorBlue BackgroundColor = "blue" )`, }, { label: "Python", language: "python", code: `BackgroundColor = Union[Literal["red", "green", "blue"], UnrecognizedStr]`, }, { label: "C#", language: "csharp", code: `// Known values mimic enum semantics var green = BackgroundColor.GREEN; green.IsKnown(); // true // Use .Of() to create an open-enum instance from any string value var purple = BackgroundColor.Of("purple"); purple.IsKnown(); // false // Alternatively, the implicit operator can be used BackgroundColor red = "red"; red.IsKnown(); // true // Enum-like equality checks are also available red.Equals(BackgroundColor.RED); // true green == BackgroundColor.Of("green"); // true`, }, { label: "Java", language: "java", code: `// Known values mimic enum semantics BackgroundColor green = BackgroundColor.GREEN; // Use .of() to create an open-enum instance from any string value BackgroundColor purple = BackgroundColor.of("purple"); purple.isKnown(); // false // Enum-like equality checks are also available BackgroundColor.of("blue").equals(BackgroundColor.BLUE); // true green == BackgroundColor.of("green"); // true`, } ]} /> ## Native enums vs union of strings Languages like Python and TypeScript support string or integer literal unions as well as native enum types. When generating SDKs for these targets, specify the preferred style using the `enumFormat` option in the `.speakeasy/gen.yaml` config file where the SDK is generated. For example, in the `gen.yaml` file: ```yaml typescript: enumFormat: union ``` This will cause all enums to be generated as a union of strings: ```typescript type Color = "sky-blue" | "dark-gray" | "stone"; ``` ```typescript import { SDK } from "cool-sdk"; const sdk = new SDK(); const result = await sdk.themes.create({ name: "flashy", background: "dark-gray", }); ``` Whereas this: ```yaml typescript: enumFormat: enum ``` Will result in the following output: ```typescript enum Color { SkyBlue = "sky-blue", DarkGray = "dark-gray", Stone = "stone", } ``` ```typescript import { SDK } from "cool-sdk"; import { Color } from "cool-sdk/models/color"; const sdk = new SDK(); const result = await sdk.themes.create({ name: "flashy", background: Color.DarkGray, }); ``` The main trade-offs to consider between the two styles are that literal unions do not require SDK users to import any additional types or values from the SDK package. The user starts typing a string or number and their IDE's autocompletion interface will suggest from the valid set of values. Native enums need to be imported from within the SDK but benefit from having members with clear names and documentation on each. This is particularly useful when you define enums that do not map well to spoken language, such as `enum: ["+", "_", "*", "!"]`. Using the `x-speakeasy-enums` extension will allow you to customize the name of each native enum member. In TypeScript and Python, native enums are nominally typed, which means that users cannot pass in any string value they have or another native enum that overlaps with the desired one without triggering a type-checker error. In some of these instances, they may need to write some code to map values to native enum members. ### Mixing enum formats While `enumFormat` is a global setting, it is possible to mix and match the enum format on a per-schema basis using the `x-speakeasy-enum-format` extension: ```yaml # `enumFormat` is set to `union` in the gen.yaml components: schemas: BackgroundColor: type: int x-speakeasy-enum-format: enum enum: - 1 - 2 - 3 x-speakeasy-enums: 1: Red 2: Green 3: Blue ``` In this case, the `BackgroundColor` enum will be generated as a native enum in the target language, while the rest of the enums will be generated as a union of values. ## Enum value descriptions Use the `x-speakeasy-enum-descriptions` extension to add descriptions to enum values that will appear as documentation in the generated SDK code. This is particularly useful for providing context about each enum option to SDK users through IDE hints and generated documentation. The extension supports two formats: ### Array format Provide descriptions as an array that corresponds to the enum values in order: ```yaml components: schemas: ProgrammingLanguage: description: Programming language environment for the generated code. type: string x-speakeasy-enum-descriptions: - Python >= 3.10, with numpy and simpy available. - TypeScript >= 4.0 enum: - PYTHON - TYPESCRIPT ``` A lint rule ensures that the `x-speakeasy-enum-descriptions` array must be the same length as the `enum` array. ### Map format Alternatively, use a map format where keys are enum values: ```yaml components: schemas: ProgrammingLanguage: description: Programming language environment for the generated code. type: string x-speakeasy-enum-descriptions: PYTHON: Python >= 3.10, with numpy and simpy available. TYPESCRIPT: TypeScript >= 4.0 enum: - PYTHON - TYPESCRIPT ``` The map format is the preferred method when using overlays. If a new enum member is added to the OpenAPI specification that isn't in the `x-speakeasy-enum-descriptions` map, it will only cause a lint warning rather than a lint failure during generation. Both formats generate the same output with JSDoc comments: ```typescript /** * Programming language environment for the generated code. */ export enum ProgrammingLanguage { /** * Python >= 3.10, with numpy and simpy available. */ PYTHON = "PYTHON", /** * TypeScript >= 4.0 */ TYPESCRIPT = "TYPESCRIPT", } ``` This feature works primarily with `enumFormat: enum`. When using native enum types, the descriptions will be rendered as JSDoc comments that provide IDE hints and appear in generated documentation. When rendered into literal unions, whilst the description is available above the literal value in source-code, our testing implied most language servers / IDEs will not automatically present it to the user. # The `oneOf` keyword Source: https://speakeasy.com/docs/sdks/customize/data-model/oneof-schemas import { CodeWithTabs } from "@/mdx/components"; In support of the OpenAPI Specification `oneOf` schemas, Speakeasy SDKs provide language-specific implementations based on idiomatic unions (when available) or using generated supporting objects that allow type safety by using an `enum` discriminator. ## Supporting objects Assuming an OpenAPI document has a `Pet` component, consider this `oneOf` block: ```yaml oneOf: - type: string - type: integer - $ref: "/components/schemas/Pet" ``` How Speakeasy generates supporting objects for your SDK depends on the language of the SDK. (){})); } public static FetchPetRequestBody of(long value) { Utils.checkNotNull(value, "value"); return new FetchPetRequestBody(TypedObject.of(value, JsonShape.DEFAULT, new TypeReference(){})); } public static FetchPetRequestBody of(Pet value) { Utils.checkNotNull(value, "value"); return new FetchPetRequestBody(TypedObject.of(value, JsonShape.DEFAULT, new TypeReference(){})); } /** * Returns an instance of one of these types: *
    *
  • {@code java.lang.String}
  • *
  • {@code long}
  • *
  • {@code pet.store.simple.models.shared.Pet}
  • *
* *

Use {@code instanceof} to determine what type is returned. For example: * *

        * if (obj.value() instanceof String) {
        *     String answer = (String) obj.value();
        *     System.out.println("answer=" + answer);
        * }
        * 
* * @return value of oneOf type **/ public java.lang.Object value() { return value.value(); } @Override public boolean equals(java.lang.Object o) { if (this == o) { return true; } if (o == null || getClass() != o.getClass()) { return false; } FetchPetRequestBody other = (FetchPetRequestBody) o; return Objects.deepEquals(this.value.value(), other.value.value()); } @Override public int hashCode() { return Objects.hash(value.value()); } @SuppressWarnings("serial") public static final class _Deserializer extends OneOfDeserializer { public _Deserializer() { super(FetchPetRequestBody.class, false, TypeReferenceWithShape.of(new TypeReference() {}, JsonShape.DEFAULT), TypeReferenceWithShape.of(new TypeReference() {}, JsonShape.DEFAULT), TypeReferenceWithShape.of(new TypeReference() {}, JsonShape.DEFAULT)); } } @Override public String toString() { return Utils.toString(FetchPetRequestBody.class, "value", value); } }`, } ]} /> ## Requests Assume you have an operation that allows the user to fetch a pet by submitting the pet's name, ID, or complete pet object: ```yaml /pet: get: operationId: fetchPet requestBody: description: identifier of pet to fetch required: true content: application/json: schema: oneOf: - type: string - type: integer - $ref: "/components/schemas/Pet" responses: "200": description: fetched pet content: application/json: schema: $ref: "#/components/schemas/Pet" ``` ## Responses Sometimes you may have a response that specifies a `oneOf` schema. For languages that do not natively support unions, Speakeasy will create supporting objects to deserialize the `oneOf` response into the correct object type. No supported objects are needed for languages with native union types, so Speakeasy will deserialize into the native type. For example, this schema: ``` /pet_id: get: operationId: petId responses: "200": description: OK content: application/json: schema: title: res oneOf: - type: string - type: integer ``` Will result in these response types: res() { ... } ... }`, } ]} /> ### Splitting `oneOf` schema types By defining similar operations with aligned but different schemas, you can apply `x-speakeasy-type-override: any` for untyped operations and use `oneOf` to define stricter types in others. This allows you to use methods like `DoSomething(StrictObject{...})` alongside `DoSomethingUntyped({...})`, providing flexibility across SDK methods based on the required schema type. This approach is particularly useful when dealing with endpoints that require you to split `oneOf` schema types into separate SDK methods. Example: ```yaml /sources#SellerPartner: post: requestBody: content: application/json: schema: $ref: "#/components/schemas/SourceSellerPartnerCreateRequest" tags: - "Sources" responses: "200": content: application/json: schema: $ref: "#/components/schemas/SourceResponse" description: "Successful operation" "400": description: "Invalid data" "403": description: "Not allowed" operationId: "createSourceSellerPartner" summary: "Create a source" description: "Creates a source given a name, workspace ID, and a JSON blob containing the configuration for the source." x-speakeasy-entity-operation: Source_SellerPartner#create ``` # Types Source: https://speakeasy.com/docs/sdks/customize/data-model/types ## Type naming Speakeasy names types using the shortest name possible, which is achieved by deducing a name from the surrounding context. Types defined using components generally result in better type names. Where possible, Speakeasy uses the component's key name as the type name. For example, given the following schema: ```yaml components: schemas: User: type: object properties: id: type: string name: type: string ``` The type name for the `User` schema will be `User` where possible. If a conflict arises with another type in the same namespace, name resolution will kick in: The earliest encountered type will be named `User` and types encountered subsequently will have prefixes or suffixes added to the name based on context to avoid conflicts. If a component name is unavailable (for example, the type is defined inline in a schema, request, response, or parameter), Speakeasy will determine the type name based on context in the following order: - The `x-speakeasy-name-override` extension value in the schema - The `title` property in the schema - The `$anchor` property in the schema - Any other context of the schema Types that are named this way are `objects` that become classes, `integer` and `string` types that have `enum` values defined in the schema, or `oneOf` or `anyOf` schemas that become union types. Inline schemas are more likely to have name conflicts with other types, which can result in context-based prefixes or suffixes being added to type names until the conflict is resolved. For example: ```yaml paths: /users: get: operationId: getUsers responses: '200': content: application/json: schema: type: array items: type: object # inline schema that will be named based on surrounding context title: User properties: id: type: string name: type: string /user: get: operationId: getUser responses: '200': content: application/json: schema: type: object # inline scheme that will be named based on surrounding context title: User properties: id: type: string name: type: string ``` Here, both inline schemas are candidates for the name `User`. As there will be a conflict (Speakeasy doesn't perform any inference to assess whether the schemas are the same type), the second schema will be given a name with a context-based prefix to avoid a conflict with the first schema. Some of the context prefixes and suffixes that can be added to type names are: - Reference type - Reference name - Property name - Operation name - Tag name - Request - Response - Position in `oneOf` or `anyOf` schema Should Speakeasy run out of context to use in naming the type, it will add numbers to type names as suffixes. To avoid unexpected type names and ensure you get the names you expect, use unique component names for your schemas wherever possible. ## Input and output models Speakeasy will generate separate input and output models for schemas that are used in both request and response bodies and define `readOnly` and `writeOnly` flags in their properties. For example, given the following schema: ```yaml paths: /drinks/{id}: post: operationId: updateDrink parameters: - name: id in: path required: true schema: type: string requestBody: content: application/json: schema: $ref: '#/components/schemas/Drink' responses: '200': content: application/json: schema: $ref: '#/components/schemas/Drink' components: schemas: Drink: type: object properties: id: type: string readOnly: true stockUpdate: type: integer writeOnly: true name: type: string category: type: string stock: type: integer readOnly: true ``` The `Drink` component is used both as a request body and response body schema, but it uses fields that can only be set when updating the drink and read when getting the returned drink. In this case, Speakeasy will generate two models `DrinkInput` and `DrinkOutput`. The `DrinkInput` model will have the following properties: - stockUpdate - name - category The `DrinkOutput` model will have the following properties: - id - name - category - stock If a schema has only `readOnly` flags and no `writeOnly` flags or vice versa, Speakeasy will still generate two models if used in both request and response bodies, but the `Input` or `Output` schema will be added only to the relevant models based on the flags. The `Input` and `Output` suffixes can be reconfigured using the `inputModelSuffix` and `outputModelSuffix` options in the `gen.yaml` file. See the [gen.yaml reference](/docs/gen-reference) for more infomation. ## Handling constants and defaults If a schema has a `const` or `default` value defined in it, Speakeasy will generate code to handle these wherever possible. ### `const` Using `const` allows you to specify a value that must be transmitted to the server and is always expected to be received from the server. For example: ```yaml components: schemas: Drink: type: object properties: type: type: string const: 'drink' ``` The `type` property has a `const` value of `drink`, so the SDK will be generated with this field as non-configurable, as the value `drink` will always be transmitted. A `const` getter will be generated to access the value if required. ### `default` Default values allow you to specify a value to be transmitted to the server if none is provided by the end user. For example: ```yaml components: schemas: Drink: type: object properties: category: type: string default: 'spirits' required: - category ``` The `category` property has a default of `spirits`. Although `category` is marked as `required`, the SDK will be generated with this field set to optional, and the value `spirits` will be transmitted if no other value is provided by the end user. ## Examples If a type includes an `example` or `examples` field, Speakeasy will use the values (if valid for the defined schema) to populate usage snippets in the generated SDKs. If more than one example is provided in the `examples` field, a random example will be chosen. ## Including unused schema components When working with OpenAPI documents, there may be cases when you want to include schema components in your generated SDKs, even if they aren't directly referenced by any API endpoints. This is particularly useful for: - Webhook payload types in OpenAPI Specification versions predating the official webhook support introduced in OpenAPI Specification 3.1 - Types used in asynchronous operations or events - Shared types that may be used in future endpoints ### Using `x-speakeasy-include` To include an unused schema component in your generated SDK, add the `x-speakeasy-include: true` extension to the schema component definition: ```yaml components: schemas: WebhookPayload: x-speakeasy-include: true # This schema will be included in the SDK even if unused. type: object properties: event_type: type: string data: type: object ``` ### Important notes - This extension only works in the main OpenAPI document. It is not supported in referenced documents (for example, it won't work for components in separate files). - The schema will be included in your SDK regardless of whether it's referenced by any endpoints. - This is particularly useful for webhook payloads in older versions of the OpenAPI Specification without the webhook support built into OpenAPI Specification 3.1+. # Deprecations Source: https://speakeasy.com/docs/sdks/customize/deprecations import { CodeWithTabs } from "@/mdx/components"; The OpenAPI Specification allows deprecating parts of an API, such as methods, parameters, and properties. When deprecating a part of an API, the SDK will generate relevant `deprecated` annotations in the code and add a `⚠️ Deprecated` label to the SDK docs. In addition to labeling deprecated parts of an API, Speakeasy extensions are available to customize the messaging of deprecated items. ## Deprecate Methods Deprecate methods in an SDK using the `deprecated` field in the OpenAPI schema. This will add a `deprecated` annotation to the generated method and a `⚠️ Deprecated` label to the SDK docs. Use the `x-speakeasy-deprecation-message` extension to customize the deprecation message displayed in code and the SDK docs. Use the `x-speakeasy-deprecation-replacement` extension to specify the method that should be used instead of the deprecated method. ```yaml paths: /drinks: get: operationId: listDrinks deprecated: true x-speakeasy-deprecation-message: This API will be removed in our next release, please refer to the beverages endpoint. x-speakeasy-deprecation-replacement: listBeverages responses: "200": description: OK tags: - drinks /beverages: get: operationId: listBeverages responses: "200": description: OK tags: - beverages ``` {}`, }, { label: "Python", language: "python", code: `def list_drinks(self, request: operations.ListDrinksRequest) -> operations.ListDrinksResponse: r"""Get a list of drinks. Get a list of drinks, if authenticated this will include stock levels and product codes otherwise it will only include public information. Deprecated method: This API will be removed in our next release, please refer to the beverages endpoint. Use list_beverages instead. """`, }, { label: "Go", language: "go", code: ` // ListDrinks - Get a list of drinks. // Get a list of drinks, if authenticated this will include stock levels and product codes otherwise it will only include public information. // // Deprecated method: This API will be removed in our next release, please refer to the beverages endpoint. Use ListBeverages instead. func (s *Drinks) ListDrinks(ctx context.Context, request operations.ListDrinksRequest) (*operations.ListDrinksResponse, error) {}`, }, { label: "Java", language: "java", code: ` /** * Get a list of drinks. * Get a list of drinks, if authenticated this will include stock levels and product codes otherwise it will only include public information. * @param request The request object containing all of the parameters for the API call. * @return The response from the API call. * @throws Exception if the API call fails. * @deprecated method: This API will be removed in our next release, please refer to the beverages endpoint. Use listBeverages instead. */ @Deprecated public org.openapis.openapi.models.operations.ListDrinksResponse listDrinks( org.openapis.openapi.models.operations.ListDrinksRequest request) throws Exception {}`, }, { label: "C#", language: "csharp", code: `[Obsolete("This method will be removed in a future release, please migrate away from it as soon as possible. Use ListBeverages instead")] public async Task ListDrinksAsync() {}`, }, { label: "PHP", language: "php", code: `/** * Get a list of drinks, if authenticated this will include stock levels and product codes otherwise it will only include public information. * @param request The request object containing all of the parameters for the API call. * @return The response from the API call. * @throws OpenAPI\\OpenAPI\\SDKException * @deprecated method: This API will be removed in our next release, please refer to the beverages endpoint. Use listBeverages instead. */ public function listDrinks(Shared\\ListDrinksRequest $request): ListDrinksResponse`, }, { label: "Ruby", language: "ruby", code: ` # Get a list of drinks, if authenticated this will include stock levels and product codes otherwise it will only include public information. # @param request [Models::Shared::ListDrinksRequest] The request object containing all of the parameters for the API call. # @return [ListDrinksResponse] The response from the API call. # @raise [Models::Errors::APIError] # @deprecated This API will be removed in our next release, please refer to the beverages endpoint. Use list_beverages instead. def list_drinks(request)`, } ]} /> ## Deprecate Parameters Deprecate parameters in an SDK using the `deprecated` field in the OpenAPI schema. This will add a `deprecated` annotation to the corresponding field in the generated objects and remove the parameter from any relevant usage examples in the SDK docs. Use the `x-speakeasy-deprecation-message` extension to customize the deprecation message displayed in code and the SDK docs. ```yaml paths: /drinks: get: operationId: listDrinks summary: Get a list of drinks. description: Get a list of drinks, if authenticated this will include stock levels and product codes otherwise it will only include public information. tags: - drinks parameters: - name: ingredient in: query description: Filter by ingredient. required: false schema: type: string example: "vodka" - name: name in: query description: Filter by name. required: false deprecated: true x-speakeasy-deprecation-message: We no longer support filtering by name. schema: type: string example: "martini" - name: limit in: query description: Limit the number of results. required: false schema: type: integer minimum: 1 maximum: 100 example: 100 ``` ingredient; /** * Filter by name. * @deprecated field: We no longer support filtering by name. */ @SpeakeasyMetadata("queryParam:style=form,explode=true,name=name") @Deprecated private Optional name; /** * Limit the number of results. */ @SpeakeasyMetadata("queryParam:style=form,explode=true,name=limit") private Optional limit; }`, }, { label: "C#", language: "csharp", code: `public class ListDrinksRequest { /// /// Filter by ingredient. /// [SpeakeasyMetadata("queryParam:style=form,explode=true,name=ingredient")] public string? Ingredient { get; set; } /// /// Limit the number of results. /// [SpeakeasyMetadata("queryParam:style=form,explode=true,name=limit")] public long? Limit { get; set; } /// /// Filter by name. /// [Obsolete("This field will be removed in a future release, please migrate away from it as soon as possible")] [SpeakeasyMetadata("queryParam:style=form,explode=true,name=name")] public string? Name { get; set; } }`, }, { label: "PHP", language: "php", code: `class ListDrinksRequest { /** * Filter by ingredient. * @var ?string $ingredient */ #[SpeakeasyMetadata('queryParam:style=form,explode=true,name=ingredient')] private ?string ingredient; /** * Filter by name. * @var ?string name * @deprecated field: We no longer support filtering by name. */ #[SpeakeasyMetadata('queryParam:style=form,explode=true,name=name')] private ?string name; /** * Limit the number of results. * @var ?float limit */ #[SpeakeasyMetadata("queryParam:style=form,explode=true,name=limit")] private ?float limit; }`, }, { label: "Ruby", language: "ruby", code: `class ListDrinksRequest include Crystalline::MetadataFields # Filter by ingredient. field :ingredient, Crystalline::Nilable.new(::String), { 'queryParam': {'style': 'form', 'explode': true, 'name': 'ingredient'} } # Filter by name. field :name, Crystalline::Nilable.new(::String), { 'queryParam': {'style': 'form', 'explode': true, 'name': 'name'} } # Limit the number of results field :limit, Crystalline::Nilable.new(::Float), { 'queryParam': {'style': 'form', 'explode': true, 'name': 'limit'} } end`, } ]} /> ## Deprecate Properties Deprecate properties in an SDK using the `deprecated` field in the OpenAPI schema. This will add a `deprecated` annotation to the corresponding property in the generated objects and remove the property from any relevant usage examples in the SDK docs. Use the `x-speakeasy-deprecation-message` extension to customize the deprecation message displayed in code and the SDK docs. ```yaml components: schemas: Drink: type: object properties: name: type: string stock: type: integer productCode: type: string sku: type: string deprecated: true x-speakeasy-deprecation-message: We no longer support the SKU property. required: - name - stock - productCode ``` sku; }`, }, { label: "C#", language: "csharp", code: `public class Drink { [SpeakeasyMetadata("queryParam:name=name")] public string Name { get; set; } = default!; [SpeakeasyMetadata("queryParam:name=productCode")] public string ProductCode { get; set; } = default!; [Obsolete("This field will be removed in a future release, please migrate away from it as soon as possible")] [SpeakeasyMetadata("queryParam:name=sku")] public string? Sku { get; set; } [SpeakeasyMetadata("queryParam:name=stock")] public long Stock { get; set; } = default!; }`, }, { label: "PHP", language: "php", code: `class Drink { /** * @var ?string $name */ #[SpeakeasyMetadata("queryParam:name=name")] public ?string $name = null; /** * @var string productCode */ #[SpeakeasyMetadata("queryParam:name=productCode")] public string $productCode; /** * @var ?string sku * @deprecated field: This field will be removed in a future release, please migrate away from it as soon as possible */ #[SpeakeasyMetadata("queryParam:name=sku")] public ?string sku = null; /** * @var float stock */ #[SpeakeasyMetadata("queryParam:name=stock")] public float stock; }`, }, { label: "Ruby", language: "ruby", code: `class Drink include Crystalline::MetadataFields field :name, ::String, { 'queryParam': {'name':'name'} } field :product_code, ::String, { 'queryParam': {'name':'product_code'} } # @deprecated field: This field will be removed in a future release, please migrate away from it as soon as possible field :sku, Crystalline::Nilable.new(::String), { 'queryParam': {'name':'sku'} } field :stock, ::Float, { 'queryParam': {'name':'stock'} } end`, } ]} /> # Define global parameters Source: https://speakeasy.com/docs/sdks/customize/globals import { CodeWithTabs, Callout } from "@/mdx/components"; Use the `x-speakeasy-globals` extension to define parameters that can be configured globally on the main SDK instance. These parameters will be automatically populated for any operations that use them. This is especially useful for configurations that are required across all operations, such as customer IDs. ```yaml openapi: 3.1.0 info: title: Test version: 0.0.1 servers: - url: https://httpbin.org x-speakeasy-globals: parameters: - name: customerId in: path schema: type: string paths: /api/{customerId}: get: operationId: getTest parameters: - name: customerId # If this matches a global parameter it will be populated automatically in: path schema: type: string required: true responses: "200": description: OK ``` If the `name`, `in`, and `schema` values of a global parameter match a parameter in an operation, the global parameter will be populated automatically. If the global parameter is not used in the operation, it will be ignored. ## Preferred method: Using component references The preferred way to define global parameters is by referencing a component. This ensures consistency and reusability: ```yaml openapi: 3.1.0 info: title: Test version: 0.0.1 servers: - url: https://httpbin.org x-speakeasy-globals: parameters: - $ref: "#/components/parameters/CustomerId" paths: /api/{customerId}: get: operationId: getTest parameters: - $ref: "#/components/parameters/CustomerId" responses: "200": description: OK components: parameters: CustomerId: name: customerId in: path schema: type: string ``` ## Supported parameter types Global parameters can be used with `in: query`, `in: path`, or `in: header` fields. Only primitive types such as `string`, `number`, `integer`, `boolean`, and `enums` are supported for global parameters. The global parameter definitions in the sample above will generate the following output: setCustomerId("1291fbe8-4afb-4357-b1de-356b65c417ca")->build(); try { $response = $sdk->getCustomer(request=new GetCusomterRequest()); // handle response } catch (OpenAPI\\ErrorThrowable $e) { // handle exception }`, }, { label: "Ruby", language: "ruby", code: `require 'openapi' Models = ::OpenApiSDK::Models s = ::OpenApiSDK::SDK.new( customer_id: "1291fbe8-4afb-4357-b1de-356b65c417ca" ) begin response = s.get_customer(request: Models::Operations::GetCustomerRequest.new) # handle response rescue Models::Errors::APIError => e # handle exception raise e end`, } ]} /> ## Hiding global parameters from method signatures Currently, this feature is only supported in Go, Python, Java, and TypeScript. To hide global parameters from method signatures, use the `x-speakeasy-globals-hidden` extension. This is useful when you want the global parameter to be set only once during SDK instantiation and not be overridden in individual operations. ```yaml openapi: 3.1.0 info: title: Test version: 0.0.1 servers: - url: https://httpbin.org x-speakeasy-globals: parameters: - name: customerId in: path schema: type: string x-speakeasy-globals-hidden: true # This will hide the global parameter from all operations paths: /api/{customerId}: get: operationId: getTest parameters: - name: customerId in: path schema: type: string required: true responses: "200": description: OK ``` Control the visibility of the global parameter by setting `x-speakeasy-globals-hidden` to `true` on the global parameter definition or on the operation parameter that matches the global parameter. Setting it globally hides the parameter from all operations. Setting it on a specific operation hides it only for that operation. # Java SDK async migration guide Source: https://speakeasy.com/docs/sdks/customize/java/java-async-migration import { Callout } from "@/mdx/components"; The migration steps detailed below are only necessary for Java SDKs generated before version `1.606.9` that are opting into async support. ## Prerequisites To follow this guide, you need: - The Speakeasy CLI version `1.606.9` or higher - Java 11 or higher - An existing Java SDK generated by Speakeasy - Access to your `.speakeasy/gen.yaml` configuration file ## Step 1: Enable async support in configuration Enable the following flag in your `.speakeasy/gen.yaml`: ```yaml java: asyncMode: enabled ``` Then regenerate your SDK with compilation skipped: ```bash speakeasy run --skip-compile ``` This generates the async hooks and adapters necessary for Step 2. We skip compilation because the generator doesn't touch the `SDKHooks` file, and async SDK initialization requires an additional method that we need to add manually to that file. ## Step 2: Add async hook registration method The async feature introduces new hook interfaces that work with `CompletableFuture`. You'll need to add a new initialization method to your `SDKHooks` class. ### Locate your SDKHooks file Open `./src/main/java//hooks/SDKHooks.java` in your Java SDK project. ### Add the async initialize method Add this method to your `SDKHooks` class (`org.openapis.openapi` is a placeholder for your package): ```java // replace org.openapis.openapi with your actual package public static void initialize(org.openapis.openapi.utils.AsyncHooks hooks) { // register async hooks here } ``` Your `SDKHooks` class should now look like this: ```java package org.openapis.openapi.hooks; public final class SDKHooks { public static void initialize(org.openapis.openapi.utils.Hooks hooks) { // register synchronous hooks here } public static void initialize(org.openapis.openapi.utils.AsyncHooks hooks) { // register async hooks here } } ``` ## Step 3: Migrate existing hooks (if applicable) If you don't have existing hooks, skip to Step 4. If you do have hooks, choose one of the following migration options: Ensure hook parity across sync and async variants. Hooks are not automatically applied across both - you need to register them separately for each variant. ### Option A: Quick migration with HookAdapters Use `HookAdapters` to automatically convert your existing synchronous hooks to async: ```java package org.openapis.openapi.hooks; import org.openapis.openapi.utils.Hook; import org.openapis.openapi.utils.HookAdapters; import java.util.concurrent.CompletableFuture; public final class SDKHooks { // Your existing hook static Hook.BeforeRequest AUTH_AND_TRACING_HOOK = new AuthAndTracingBeforeRequest( myTokenProvider ); public static void initialize(org.openapis.openapi.utils.Hooks hooks) { // Keep existing synchronous hook registration hooks.registerBeforeRequest(AUTH_AND_TRACING_HOOK); } public static void initialize(org.openapis.openapi.utils.AsyncHooks hooks) { // Convert synchronous hook to async using adapter hooks.registerBeforeRequest(HookAdapters.toAsync(AUTH_AND_TRACING_HOOK)); } } ``` Use this option for: - A quick migration with minimal code changes - CPU-bound hooks - Low-to-moderate throughput applications HookAdapters uses the global ForkJoinPool, which may become a bottleneck for I/O-bound hooks in high-throughput applications. ### Option B: Full async implementation Reimplement your hooks using async APIs and non-blocking operations: ```java package org.openapis.openapi.hooks; import org.openapis.openapi.utils.AsyncHook; import java.util.concurrent.CompletableFuture; import java.util.logging.Logger; public final class SDKHooks { private static final Logger logger = Logger.getLogger(SDKHooks.class.getName()); // Async version of your hook with error handling static AsyncHook.BeforeRequest ASYNC_AUTH_HOOK = (context, request) -> { return authService.getTokenAsync() // Use async API .thenApply(token -> request.toBuilder() .addHeader("Authorization", "Bearer " + token) .build()) .exceptionally(throwable -> { // Handle auth failures gracefully logger.warning("Auth token retrieval failed: " + throwable.getMessage()); return request; // Return original request }); }; public static void initialize(org.openapis.openapi.utils.Hooks hooks) { // Keep existing synchronous hooks if needed } public static void initialize(org.openapis.openapi.utils.AsyncHooks hooks) { // Register native async hooks hooks.registerBeforeRequest(ASYNC_AUTH_HOOK); } } ``` Use this option for: - High-throughput applications - I/O-bound operations (such as HTTP calls and database queries) - Meeting maximum performance requirements ## Step 4: Compile the SDK After making the configuration and code changes, run the following command again to compile and verify that the changes are correct: ``` speakeasy run ``` Run `speakeasy run` to compile your SDK with async support! Then test your application to ensure the async functionality works as expected. ## Available async hook interfaces The async support provides three hook interfaces that mirror the synchronous ones: ### AsyncHook.BeforeRequest ```java import java.util.concurrent.CompletableFuture; CompletableFuture beforeRequest( Hook.BeforeRequestContext context, HttpRequest request ); ``` ### AsyncHook.AfterSuccess ```java import java.util.concurrent.CompletableFuture; CompletableFuture> afterSuccess( Hook.AfterSuccessContext context, HttpResponse response ); ``` ### AsyncHook.AfterError ```java import java.util.concurrent.CompletableFuture; CompletableFuture> afterError( Hook.AfterErrorContext context, HttpResponse response, Throwable error ); ``` ## Best practices - **Choose the right migration option** based on your application's throughput requirements and hook complexity. - **Test thoroughly** after migration to ensure the async behavior meets your expectations. - **Monitor performance** to validate that async support provides the expected benefits. - **Use appropriate async APIs** in Option B implementations (such as async HTTP clients and reactive database drivers). ## Troubleshooting If you encounter these issues, refer to the troubleshooting instructions below: ### Missing async initialize method Ensure you've added the `initialize(AsyncHooks hooks)` method to your `SDKHooks` class. ### Performance issues with HookAdapters Consider migrating to Option B for I/O-bound hooks in high-throughput scenarios. ## Next steps After a successful migration, your Java SDK will support both synchronous and asynchronous operations, allowing you to leverage non-blocking I/O for improved performance in concurrent applications. For more information on Java SDK configuration and features, check out the [Java SDK documentation](/docs/languages/java/methodology-java). # Parameter encoding Source: https://speakeasy.com/docs/sdks/customize/java/param-encoding ## The `allowReserved` setting OpenAPI 3.x supports the `allowReserved` setting, which applies exclusively to query parameters. This allows reserved characters, such as `:/?#[]@!$&'()*+,;=`, to appear unencoded in request URLs. OpenAPI 3.x does not support the `allowedReserved` setting for path parameters, although API owners may occasionally want to model this behavior. Consider a URL with a path parameter `item`, such as `https://stuff.com/store/{item}`. The API might be designed to accept values like `most-popular` or `book/most-popular` for `{item}`, where the `/` character remains unencoded, resulting in a URL like `https://stuff.com/store/book/most-popular`. The Speakeasy OpenAPI extension `x-speakeasy-param-encoding-override: allowReserved` can be applied to a path parameter to allow reserved characters, such as `:/?#[]@!$&'()*+,;=`, to appear unencoded in the URL. Here's an example demonstrating the use of the path parameter encoding extension: ```yaml /store/{item}: get: operationId: item parameters: - name: item in: path schema: type: string example: most-popular required: true x-speakeasy-param-encoding-override: allowReserved responses: "200": ... ``` As of November 2024, the `x-speakeasy-param-encoding-override` extension is supported for Java. Contact support for availability in other languages. # Spring Boot Integration for Java SDKs Source: https://speakeasy.com/docs/sdks/customize/java/spring-boot-integration import { Callout } from "@/mdx/components"; import { FileTree } from "nextra/components"; Speakeasy auto-generates [Spring Boot starters](https://docs.spring.io/spring-boot/docs/current/reference/html/using.html#using.build-systems.starters) alongside Java SDKs, enabling **zero-configuration integration** with **sensible defaults**. Set `generateSpringBootStarter: false` in your `gen.yaml` to disable. Enabled by default. ## Module Structure Three modules provide complete Spring Boot integration: - **Auto-configuration**: Bean registration with [`@ConditionalOnMissingBean`](https://docs.spring.io/spring-boot/docs/current/api/org/springframework/boot/autoconfigure/condition/ConditionalOnMissingBean.html) - **Properties**: [`@ConfigurationProperties`](https://docs.spring.io/spring-boot/docs/current/reference/html/features.html#features.external-config.typesafe-configuration-properties) from OpenAPI spec - **Starter**: [Dependency aggregator](https://docs.spring.io/spring-boot/docs/current/reference/html/using.html#using.build-systems.starters) ## Configuration Properties Type-safe `@ConfigurationProperties` are auto-generated from your OpenAPI spec, namespaced under `projectId`: ```java @ConfigurationProperties(prefix = "my-api") // From your gen.yaml projectId public class MyApiProperties { // Core Configuration private String serverUrl; // From OpenAPI servers section private String server; // Server name (if using named servers) private int serverIdx = 0; // Server index (0-based, defaults to first server) // Server Variables (from OpenAPI spec) private ServerVariables serverVariables = new ServerVariables(); // Security Configuration (flattened from OpenAPI security schemes) private Security security = new Security(); // Global Parameters (from OpenAPI components) private Globals globals = new Globals(); // SDK Behavior Configuration private RetryConfig retryConfig = new RetryConfig(); private HttpClient httpClient = new HttpClient(); } ``` **Property Mapping:** - **Servers**: Indexed (`serverIdx`) or named (`server`) with variables as enums - **Security**: Flattened schemes (API keys, OAuth, HTTP auth) - **Global Parameters**: Headers, query params, path params from spec - **SDK Behavior**: Retry policies, HTTP client settings - **Rich Types**: OpenAPI enums → Java enums, duration strings → `Duration` objects ## Auto-Configuration Beans are conditionally registered based on classpath and properties: ```java @AutoConfiguration @ConditionalOnClass(SDK.class) @EnableConfigurationProperties(MyApiProperties.class) public class MyApiAutoConfiguration { @Bean @ConditionalOnMissingBean public SDK sdk(SDKConfiguration config) { return new SDK(config); // Main SDK bean } @Bean // Individual sub-SDKs automatically available @ConditionalOnMissingBean public UsersSDK usersSDK(SDK sdk) { return sdk.users(); } @Bean // Conditional on property presence @ConditionalOnProperty(prefix = "my-api.retry-config", name = "strategy") public RetryConfig retryConfig(MyApiProperties properties) { return convertToRetryConfig(properties.getRetryConfig()); } @Bean // Conditional on security configuration @ConditionalOnPropertyPrefix(prefix = "my-api.security") public SecuritySource securitySource(MyApiProperties properties) { return buildSecurityFromProperties(properties.getSecurity()); } } ``` **Bean Registration:** - **Core SDK**: Always present with starter - **Sub-SDKs**: Per API group (Users, Orders, etc.) - **Security**: Conditional on `my-api.security.*` - **Retry**: Conditional on `my-api.retry-config.strategy` - **Async**: When async mode enabled ## Usage Configure via `application.yml`: ```yaml my-api: # Namespace from your projectId server-url: "https://api.example.com" server-variables: # Rich type mapping with enum support environment: PROD # Enum values provide IDE dropdown selection region: US_EAST_1 # Type-safe enum prevents configuration errors security: # Structured security configuration api-key: "${API_KEY}" oauth: client-id: "${CLIENT_ID}" client-secret: "${CLIENT_SECRET}" retry-config: # SDK behaviors with readable duration formats strategy: BACKOFF # Enum with IDE autocomplete backoff: initial-interval: 500ms # Duration parsing: ms, s, m, h max-interval: 30s # Human-readable format max-elapsed-time: 5m # Automatically converted to Duration objects globals: # Global parameters from OpenAPI spec user-agent: "MyApp/1.0" api-version: "2023-10" http-client: # HTTP behavior configuration enable-debug-logging: false redacted-headers: ["Authorization", "X-API-Key"] ``` Inject and use with zero boilerplate: ```java @RestController public class UserController { @Autowired private UsersSDK usersSDK; // Direct sub-SDK injection @Autowired private AsyncUsersSDK asyncUsersSDK; // Async variant // Synchronous usage @GetMapping("/users/{id}") public User getUser(@PathVariable String id) { return usersSDK.getUser() .userId(id) .call(); } // Reactive usage with WebFlux @GetMapping("/users/{id}/async") public Mono getUserAsync(@PathVariable String id) { return Mono.fromFuture( asyncUsersSDK.getUser() .userId(id) .call() // Returns CompletableFuture ); } // Streaming data @GetMapping(value = "/users/stream", produces = "application/x-ndjson") public Flux streamUsers(@RequestParam String department) { Publisher userStream = asyncUsersSDK.listUsers() .department(department) .callAsPublisher(); // Returns Publisher for pagination return Flux.from(userStream); } } ``` ## WebFlux Integration Async SDKs integrate seamlessly with [Spring WebFlux](https://docs.spring.io/spring-framework/docs/current/reference/html/web-reactive.html): - `CompletableFuture` → `Mono` conversion - `Publisher` → `Flux` for streaming (pagination, SSE, JSONL) - Non-blocking [reactive streams](https://docs.spring.io/spring-framework/docs/current/reference/html/web-reactive.html#webflux-reactive-streams) support ## Publishing **No workflow changes required.** Gradle auto-configures additional artifacts: - `{project-name}-spring-boot-autoconfigure-{version}.jar` - `{project-name}-spring-boot-starter-{version}.jar` Published alongside your main SDK to existing repositories. ## Benefits **For API Producers:** - Zero deployment overhead - Reduced support via familiar [Spring Boot patterns](https://docs.spring.io/spring-boot/docs/current/reference/html/using.html) - Faster adoption by Spring developers - Enterprise appeal with [Actuator](https://docs.spring.io/spring-boot/docs/current/reference/html/actuator.html) and ecosystem compatibility **For SDK Users:** - Add starter dependency, configure via `application.yml` - Type-safe properties with IDE support - Rich type mapping (enums, durations) - WebFlux integration when async enabled **Compatibility:** Spring Boot 2.x/3.x, Java 11+ # Customize methods Source: https://speakeasy.com/docs/sdks/customize/methods import { CodeWithTabs } from "@/mdx/components"; ## Change method names Speakeasy uses your OpenAPI schema to infer names for class types, methods, and parameters. However, you can override these names to tailor the generated SDK to your preferences. The `x-speakeasy-name-override` extension can be used to override the name of a class, method, or parameter. Depending on where this extension is placed in an OpenAPI schema, names can be overridden at different scopes, such as globally or for specific operations and parameters. For example, the `x-speakeasy-name-override` extension can be used to override the generated name for a method generated from an operation. This extension can be applied globally by placing it at the root of the OpenAPI schema, allowing all methods with an `operationId` that matches the provided `operationId` regex to be overridden with `methodNameOverride`. ```yaml openapi: 3.1.0 info: title: Test version: 0.0.1 servers: - url: https://httpbin.org security: - basicAuth: [] x-speakeasy-name-override: - operationId: ^get.* methodNameOverride: get - operationId: ^post.* methodNameOverride: create paths: /test: get: operationId: getTest responses: "200": description: OK post: operationId: postTest requestBody: required: true content: application/json: schema: $ref: "#/components/schemas/Test" responses: "200": description: OK ``` Since `getTest` and `postTest` match the `^get.*` and `^post.*` regexes defined by the global `x-speakeasy-name-override` extension, these method names will be generated as `get` and `create`, respectively. Alternatively, `x-speakeasy-name-override` can be used at the operation level to override the generated method name for that specific operation only. If the OpenAPI schema includes the extension at both the global and operation levels, the operation-level extension will take precedence. Consider the same schema shown above with an operation-level extension added to the `get` operation: ```yaml --- get: operationId: getTest x-speakeasy-name-override: getRandomTest responses: "200": description: OK ``` Now, `postTest` will be generated as `create` as before, but `getTest` will be generated as `getRandomTest`. ## Configuring method signatures To customize method signatures in an SDK, control how parameters are passed to the method by setting the `maxMethodParams` configuration option in the `gen.yaml` file. Here is an example of how to set the `maxMethodParams` configuration option in your `gen.yaml` file: ```yaml configVersion: 2.0.0 generation: # ... typescript: maxMethodParams: 4 # ... ``` Here, the `maxMethodParams` configuration option is set to `4`, so the generated SDK will have a maximum of four parameters for each method, including the request body parameter. If the number of parameters for a method exceeds the `maxMethodParams` configuration option, the generated SDK will use a single request object parameter to encapsulate all the parameters. To ensure the generator always creates a request object for an SDK, set `maxMethodParams` to `0`. This approach is useful for enabling request objects to evolve gracefully, avoiding breaking changes to the method signature when adding parameters in the future. Here are examples of an SDK with `maxMethodParams` set to `4` and `0`: build(); $response = $sdk->drinks->listDrinks("vodka", "martini", 100); // handle response // Example of SDK with maxMethodParams set to 0 declare(strict_types=1); require 'vendor/autoload.php'; use OpenAPI\\OpenAPI; $sdk = OpenAPI\\SDK::builder()->build(); $request = new Shared\\ListDrinksRequest(ingredient="vodka", name="martini", limit=100); $response = $sdk->drinks->listDrinks($request); // handle response`, }, { label: "Ruby", language: "ruby", code: `# Example of SDK with maxMethodParams set to 4 require 'openapi' Models = ::OpenApiSDK::Models s = ::OpenApiSDK::SDK.new response = s.drinks.list_drinks("vodka", "martini", 100) # handle response # Example of SDK with maxMethodParams set to 0 require 'openapi' Models = ::OpenApiSDK::Models s = ::OpenApiSDK::SDK.new request = Models::Shared::ListDrinksRequest.new( ingredient: "vodka", name: "martini", limit: 100 ) response = s.drinks.list_drinks(request) # handle response `, } ]} /> You can also set `maxMethodParams` using the `x-speakeasy-max-method-params` extension in your OpenAPI document, either globally at the root of the document or at the operation level. The order of precedence for configuration is: - Operation-level `x-speakeasy-max-method-params` - Global-level `x-speakeasy-max-method-params` - The `maxMethodParams` configuration option in the `gen.yaml` file The configuration set in `gen.yaml` or through the extension at the root of the document will apply to all operations unless an operation-level extension overrides it. ### Exclude parameters from signatures To exclude certain parameters from the generated SDK, use the `x-speakeasy-ignore` extension. The following example uses `x-speakeasy-ignore: true` to exclude a parameter: ```yaml paths: /test/user/{user_id}: parameters: - name: user_id in: path required: true schema: type: string - name: status x-speakeasy-ignore: true in: query required: true schema: type: string get: operationId: getUser responses: "200": description: OK ... ``` ## Exclude methods from an SDK Use the `x-speakeasy-ignore` extension to exclude certain methods from the generated SDK. The following example illustrates several instances of `x-speakeasy-ignore: true` used across a schema. ```yaml paths: /test: get: x-speakeasy-ignore: true operationId: getTest responses: "200": description: OK ``` # Async Hooks for Python Source: https://speakeasy.com/docs/sdks/customize/python/async-hooks import { Callout, Table } from "@/mdx/components"; Python SDKs support async hooks for non-blocking I/O operations in async methods. When your SDK uses async operations (e.g., `await sdk.some_operation_async()`), async hooks prevent blocking the event loop during hook execution. ## When to Use Async Hooks Use async hooks when your hooks need to perform I/O operations in async SDK methods: - Fetching tokens from external authentication services - Logging to external services (e.g., Datadog, Splunk) - Caching with async clients (e.g., Redis with aioredis) - Sending telemetry to monitoring services For simple, CPU-bound operations (like modifying headers), sync hooks work fine and are automatically adapted. ## Enabling Async Hooks Add `useAsyncHooks: true` to your `gen.yaml`: ```yaml python: useAsyncHooks: true ``` This generates additional async hook infrastructure alongside the existing sync hooks. ## Hook Registration Files When async hooks are enabled, you have two registration files:
### Registering Async Hooks In `asyncregistration.py`: ```python from .asynctypes import AsyncHooks from .my_async_hook import MyAsyncHook def init_async_hooks(hooks: AsyncHooks): """Register async hooks for async SDK methods.""" hooks.register_before_request_hook(MyAsyncHook()) hooks.register_after_success_hook(MyAsyncHook()) hooks.register_after_error_hook(MyAsyncHook()) ``` ## Async Hook Interfaces Async hooks implement these interfaces from `asynctypes.py`: | Interface | Method | Purpose | |-----------|--------|---------| | `AsyncBeforeRequestHook` | `async def before_request(...)` | Modify requests before sending | | `AsyncAfterSuccessHook` | `async def after_success(...)` | Process successful responses | | `AsyncAfterErrorHook` | `async def after_error(...)` | Handle errors and failed responses | SDK initialization (`__init__`) is always synchronous in Python. Use the regular `SDKInitHook` from `types.py` for initialization logic like wrapping HTTP clients. ## Complete Example Here's a complete async hook implementation: ```python import httpx from typing import Optional, Tuple, Union from .asynctypes import ( AsyncBeforeRequestHook, AsyncAfterSuccessHook, AsyncAfterErrorHook, ) from .types import BeforeRequestContext, AfterSuccessContext, AfterErrorContext class MyAsyncHook(AsyncBeforeRequestHook, AsyncAfterSuccessHook, AsyncAfterErrorHook): """Example async hook with non-blocking I/O operations.""" async def before_request( self, hook_ctx: BeforeRequestContext, request: httpx.Request ) -> Union[httpx.Request, Exception]: """Modify request before sending. Access hook context for: - hook_ctx.operation_id: The API operation being called - hook_ctx.base_url: The base URL for the request - hook_ctx.config: Full SDK configuration (if sdkHooksConfigAccess enabled) """ # Example: Fetch token from async cache/service token = await self._fetch_token_async() # Modify request headers request.headers["Authorization"] = f"Bearer {token}" request.headers["X-Request-ID"] = await self._generate_request_id() return request async def after_success( self, hook_ctx: AfterSuccessContext, response: httpx.Response ) -> Union[httpx.Response, Exception]: """Process successful response. Return the response to continue, or an Exception to raise an error. """ # Example: Async logging to external service await self._log_request_async( operation_id=hook_ctx.operation_id, status_code=response.status_code, latency_ms=response.elapsed.total_seconds() * 1000, ) return response async def after_error( self, hook_ctx: AfterErrorContext, response: Optional[httpx.Response], error: Optional[Exception], ) -> Union[Tuple[Optional[httpx.Response], Optional[Exception]], Exception]: """Handle errors and failed responses. Return (response, error) tuple to continue processing, or raise an Exception to abort immediately. """ # Example: Report error to async monitoring service await self._report_error_async( operation_id=hook_ctx.operation_id, error=error, status_code=response.status_code if response else None, ) return (response, error) # Helper methods (would be implemented with actual async I/O) async def _fetch_token_async(self) -> str: # Fetch from Redis, external auth service, etc. ... async def _generate_request_id(self) -> str: ... async def _log_request_async(self, **kwargs) -> None: ... async def _report_error_async(self, **kwargs) -> None: ... ``` ## Automatic Sync-to-Async Adaptation Existing sync hooks automatically work in async contexts. When you register a sync hook, it's wrapped to run in a thread pool via `asyncio.to_thread()` (Python 3.9+) or `run_in_executor()` (Python 3.7-3.8). **How it works:** 1. You register a sync hook in `registration.py` 2. The SDK auto-creates an adapter in the async hooks registry 3. In async methods, the adapter runs your sync hook in a thread pool ```python # registration.py - your existing sync hook def init_hooks(hooks: Hooks): hooks.register_before_request_hook(MySyncHook()) # When async method is called: # await sdk.operation_async() # → Adapter wraps MySyncHook # → Runs via asyncio.to_thread() (non-blocking) ``` **This means:** - Sync methods use sync hooks directly (no overhead) - Async methods automatically adapt sync hooks (thread pool overhead) - No code changes required for backward compatibility ## Migration Path You can migrate from sync to async hooks incrementally: | Stage | Sync Methods | Async Methods | |-------|--------------|---------------| | **Start** | Sync hooks | Sync hooks (auto-adapted) | | **Partial migration** | Sync hooks | Mix of native async + adapted sync | | **Complete** | Sync hooks | Native async hooks | Native async hooks are more efficient than adapted sync hooks in async contexts. Adapted hooks incur thread pool overhead for each hook invocation. For I/O-heavy hooks called frequently, native async implementations provide better performance. ## Mixing Sync and Async Hooks You can register both sync and async hooks. They're invoked in registration order: ```python # asyncregistration.py def init_async_hooks(hooks: AsyncHooks): # Native async hook (best performance) hooks.register_before_request_hook(MyAsyncAuthHook()) # Adapted sync hook (runs in thread pool) from .adapters import SyncToAsyncBeforeRequestAdapter from .my_sync_hook import MySyncLoggingHook hooks.register_before_request_hook( SyncToAsyncBeforeRequestAdapter(MySyncLoggingHook()) ) ``` ## Adapters Reference The SDK provides bidirectional adapters in `adapters.py`: | Adapter | Purpose | |---------|---------| | `SyncToAsyncBeforeRequestAdapter` | Run sync `BeforeRequestHook` in async context | | `SyncToAsyncAfterSuccessAdapter` | Run sync `AfterSuccessHook` in async context | | `SyncToAsyncAfterErrorAdapter` | Run sync `AfterErrorHook` in async context | | `AsyncToSyncBeforeRequestAdapter` | Run async hook in sync context (creates event loop) | | `AsyncToSyncAfterSuccessAdapter` | Run async hook in sync context | | `AsyncToSyncAfterErrorAdapter` | Run async hook in sync context | Async-to-sync adapters create a new event loop per invocation via `asyncio.run()`. This is inefficient and should be avoided in production. Prefer native sync hooks for sync contexts. # Customize Error Handling Source: https://speakeasy.com/docs/sdks/customize/responses/errors Below is a structured guide on how to configure and customize error handling in Speakeasy-generated SDKs. ## Default Error Handling (no configuration) By default, Speakeasy SDKs handle errors as follows: 1. **Non-2xx Status Codes**: When receiving an HTTP error response, the SDK throws either: - A **custom SDK error** with typed error schemas (hoisted properties for convenient access) - A **default SDK error** that encapsulates the raw response when no custom error schema is defined 2. **Validation Errors**: If error response parsing fails or doesn't match the expected schema, validation errors are thrown. 3. **Network/IO Errors**: Connection failures, timeouts, DNS errors, and TLS errors are escalated verbatim.
Example OpenAPI file ```yaml openapi: 3.1.0 info: title: The Speakeasy Bar version: 1.0.0 servers: - url: https://speakeasy.bar paths: /drinks: get: operationId: listDrinks summary: Get a list of drinks responses: "200": description: A list of drinks content: application/json: schema: type: array items: $ref: "#/components/schemas/Drink" components: schemas: Drink: type: object title: Drink properties: name: type: string ```
TypeScript SDK Default Error Handling ```typescript import { Drinks } from "drinks"; import { SDKValidationError, SDKError, HTTPClientError, } from "drinks/models/errors/index.js"; const drinks = new Drinks(); async function run() { let result; try { result = await drinks.listDrinks(); console.log(result); } catch (err) { // 1. Default SDK Errors: Non-2xx responses without custom error schemas // Use `typescript.defaultErrorName` to change the name of `SDKError` in `gen.yaml` if (err instanceof SDKError) { console.error(err.statusCode); console.error(err.message); console.error(err.body); // Raw response body as string return; } // 2. Validation Errors: Error response parsing failed if (err instanceof SDKValidationError) { // Raw value will be type `unknown` console.error(err.rawValue); // Validation errors can be pretty-printed console.error(err.pretty()); return; } // 3. Network/IO Errors: Connection failures, timeouts, DNS errors (escalated verbatim) if (err instanceof HTTPClientError) { console.error(err.name); console.error(err.message); return; } throw err; } } ```
## Recommended Configuration To improve the DX for the end user of the SDK, it is recommended to have named error classes with structured schemas for specific error types (e.g., `BadRequestError`, `UnauthorizedError`, `NotFoundError`). APIs commonly return structured JSON errors for 4XX responses. Here is an example of how to configure this in an OpenAPI document: ```yaml openapi: 3.1.0 info: title: The Speakeasy Bar version: 1.0.0 servers: - url: https://speakeasy.bar paths: /drinks: get: operationId: listDrinks summary: Get a list of drinks responses: "200": description: A list of drinks content: application/json: schema: type: array items: $ref: "#/components/schemas/Drink" "400": description: Bad Request content: application/json: schema: $ref: "#/components/schemas/BadRequestError" "401": description: Unauthorized content: application/json: schema: $ref: "#/components/schemas/UnauthorizedError" "404": description: Not Found content: application/json: schema: $ref: "#/components/schemas/NotFoundError" components: schemas: Drink: type: object title: Drink properties: name: type: string BadRequestError: type: object title: BadRequestError properties: statusCode: type: integer error: type: string typeName: type: string message: type: string x-speakeasy-error-message: true detail: oneOf: - type: string - type: object ref: type: string UnauthorizedError: type: object title: UnauthorizedError properties: message: type: string x-speakeasy-error-message: true code: type: string NotFoundError: type: object title: NotFoundError properties: message: type: string x-speakeasy-error-message: true resource: type: string resourceId: type: string ``` Note, defining 5XX responses is generally not recommended as the server is not always in control of the response. If a JSON schema is specified for a 5XX response and the response doesn't match the schema, the SDK will raise a `SDKValidationError`. Note the use of `x-speakeasy-error-message: true` to configure the error message to be used by the SDK, which will be propagated to `err.message` in the SDK.
TypeScript SDK Custom Error Handling ```typescript import { Drinks } from "drinks"; import { SDKValidationError, SDKError, HTTPClientError, BadRequestError, UnauthorizedError, NotFoundError, } from "drinks/models/errors/index.js"; const drinks = new Drinks(); async function run() { let result; try { result = await drinks.listDrinks(); console.log(result); } catch (err) { // Custom typed errors with structured schemas and hoisted properties if (err instanceof BadRequestError) { // Access hoisted properties directly console.error(err.message); console.error(err.typeName); console.error(err.detail); console.error(err.ref); // Or access the full error data object console.error(err.data$); return; } if (err instanceof UnauthorizedError) { // Access structured error fields console.error(err.message); console.error(err.code); return; } if (err instanceof NotFoundError) { // Access resource-specific error details console.error(err.message); console.error(err.resource); console.error(err.resourceId); return; } // Default SDK Errors: Non-2xx responses without custom error schemas if (err instanceof SDKError) { console.error(err.statusCode); console.error(err.message); console.error(err.body); return; } // Validation Errors: Error response parsing failed if (err instanceof SDKValidationError) { console.error(err.rawValue); console.error(err.pretty()); return; } // Network/IO Errors: Connection failures (escalated verbatim) if (err instanceof HTTPClientError) { console.error(err.name); console.error(err.message); return; } throw err; } } ```
## Advanced Configuration ### Renaming Generated Error Classes Any unhandled API Error will raise a exception of the default `SDKError`/`APIError`/`APIException` class depending on the SDK language. To change the name of the default error class, edit the `defaultErrorName` parameter in the `gen.yaml` file for the corresponding SDK language: ```yaml python: defaultErrorName: MyError ``` To rename other generated error classes, please refer to the [Customizing Types](/docs/customize-sdks/types) documentation to rename generated error classes. ### Handling the Default Error Response The `default` response code is a catch-all for any status code not explicitly defined. By default, Speakeasy SDKs treat default responses as non-error responses. To treat it as a specific error type, define the default response in the `x-speakeasy-errors` extension on any operation: ```yaml x-speakeasy-errors: statusCodes: - "default" ``` ### Disabling Default Error Handling In certain cases, you may want to disable the default error handling behavior of SDKs. For example, you may not want to throw an error for a 404 status code. The `x-speakeasy-errors` extension can be used to override the default error-handling behavior of SDKs. Apply the `x-speakeasy-errors` extension at the `paths`, `path item`, or `operation` level. Deeper levels merge or override parent behavior. The `x-speakeasy-errors` extension is an object with the following properties: import { Table } from "@/mdx/components";
If the `statusCodes` array contains undocumented status codes, the SDK returns an SDK error object with the status code, response body as a string, and the raw response object. Otherwise, if `content-type` is `application/json`, it returns an error object from the response object in the OpenAPI document. Example: ```yaml paths: x-speakeasy-errors: statusCodes: # Defines status codes to handle as errors for all operations - 4XX # Wildcard to handle all status codes in the 400-499 range - 5XX /drinks: x-speakeasy-errors: override: true # Forces this path and its operations to only handle 404 and 500 as errors, overriding the parent x-speakeasy-errors extension at the paths level statusCodes: - 404 - 500 get: x-speakeasy-errors: statusCodes: # As override is not set to true, this operation will handle 404, 401, 500, and 503 as errors, merging with the parent x-speakeasy-errors extension at the path item level - 401 - 503 operationId: getDrinks responses: 200: description: OK content: application/json: schema: type: array items: $ref: "#/components/schemas/Drink" 401: description: Unauthorized content: application/json: # As an application/json response is defined, the schema will generate a custom error object (for example `AuthError`) that will be returned and can be tested for schema: $ref: "#/components/schemas/AuthError" 404: description: Not Found # As no application/json response is defined, the SDK will return a standard SDK error object. 500: description: Internal Server Error content: application/json: schema: $ref: "#/components/schemas/Error" 503: description: Service Unavailable ``` Another way to disable default error handling is to set the `clientServerStatusCodesAsErrors` option to `false` in the `gen.yaml` file for the SDK language: ```yaml go: clientServerStatusCodesAsErrors: false ``` # Customize responses Source: https://speakeasy.com/docs/sdks/customize/responses/responses import { CodeWithTabs } from "@/mdx/components"; ## Response formats When generating SDKs, response formats determine the structure of response types in supported languages. Three available response formats are available to choose from. Configure the response format for a given target in the `gen.yaml` file: ```yaml typescript: # Python and Go can be configured in a similar way responseFormat: flat # Or envelope-http, or envelope packageName: @acme/super-sdk version: 0.1.0 author: Speakeasy templateVersion: v2 clientServerStatusCodesAsErrors: true maxMethodParams: 4 flattenGlobalSecurity: true inputModelSuffix: input outputModelSuffix: output additionalDependencies: dependencies: {} devDependencies: {} peerDependencies: {} imports: option: openapi paths: callbacks: models/callbacks errors: models/errors operations: models/operations shared: models/components webhooks: models/webhooks ``` The following sections will reference this specification: ```yaml /payments/{id}: get: operationId: getPayment parameters: - name: id in: path required: true schema: type: string responses: "200": description: Details about a payment content: application/json: schema: $ref: "#/components/schemas/Payment" components: schemas: Payment: type: object required: [id,amount,currency] properties: id: type: integer amount: type: number currency: type: string ``` ### `responseFormat: flat` The flat response format is the simplest and most ergonomic option, as it avoids generating a wrapper type, giving SDK users direct access to the response value. When `responseFormat: flat` is enabled, the generated SDK code will return the `Payment` type directly with no indirection: {} }`, }, { label: "Python", language: "python", code: `class SDK: def get_payment(self, id: str) -> components.Payment:`, }, { label: "Go", language: "go", code: `func (s *SDK) GetPayment(ctx context.Context, id string) (*components.Payment, error) {}`, }, { label: "C#", language: "csharp", code: `public async Task GetPaymentAsync(string id){}`, }, { label: "PHP", language: "php", code: `public function getPayment(string $id, ?Options $options = null): Payment`, }, { label: "Ruby", language: "ruby", code: `sig { params(id: ::String).returns(Payment) } def get_payment(id:)`, }, ]} /> To debug HTTP metadata, users can pass a [custom client](/docs/customize-sdks/custom-http-client) to the SDK instance. ### `responseFormat: envelope-http` The `envelope-http` format builds response types with a wrapper that holds the response value and associated HTTP metadata. When `envelope-http` is enabled, the generated SDK code will produce the response types below: {} } /*--------------------------------*/ export type GetPaymentResponse = { httpMeta: components.HTTPMetadata; /** * Details about a payment */ payment?: components.Payment | undefined; }; /*--------------------------------*/ export type HTTPMetadata = { /** * Raw HTTP response; suitable for custom response parsing */ response: Response; /** * Raw HTTP request; suitable for debugging */ request: Request; };`, }, { label: "Python", language: "python", code: `class SDK: def get_payment(self, id: str) -> operations.GetPaymentResponse: #################################### @dataclasses.dataclass class GetPaymentResponse: http_meta: components_httpmetadata.HTTPMetadata = dataclasses.field() payment: Optional[components_payment.Payment] = dataclasses.field(default=None) r"""Details about a payment""" #################################### @dataclass_json(undefined=Undefined.EXCLUDE) @dataclasses.dataclass class HTTPMetadata: response: requests.Response = dataclasses.field(metadata={'dataclasses_json': { 'exclude': lambda f: True }}) r"""Raw HTTP response; suitable for custom response parsing""" request: requests.Request = dataclasses.field(metadata={'dataclasses_json': { 'exclude': lambda f: True }}) r"""Raw HTTP request; suitable for debugging"""`, }, { label: "Go", language: "go", code: `func (s *SDK) GetPayment(ctx context.Context, id string) (*operations.GetPaymentResponse, error) {} /*--------------------------------*/ type GetPaymentResponse struct { HTTPMeta components.HTTPMetadata // Details about a payment Payment *components.Payment } /*--------------------------------*/ type HTTPMetadata struct { // Raw HTTP response; suitable for custom response parsing Response *http.Response \`json:"-"\` // Raw HTTP request; suitable for debugging Request *http.Request \`json:"-"\` }`, }, { label: "C#", language: "csharp", code: `Task GetPaymentAsync(string id); /*--------------------------------*/ public class GetPaymentResponse { public HTTPMetadata HttpMeta { get; set; } = default!; // Details about a payment public Payment? Payment { get; set; } = default!; } /*--------------------------------*/ public class HTTPMetadata { /// /// Raw HTTP response; suitable for custom response parsing /// [JsonProperty("-")] public HttpResponseMessage Response { get; set; } = default!; /// /// Raw HTTP request; suitable for debugging /// [JsonProperty("-")] public HttpRequestMessage Request { get; set; } = default!; }`, }, { label: "PHP", language: "php", code: ` class SDK { public function getPayment(string $id, ?Options $options = null): GetPaymentResponse } /*--------------------------------*/ class GetPaymentResponse { /** * * @var Shared\\HTTPMetadata $httpMeta */ #[\\Speakeasy\\Serializer\\Annotation\\Exclude] public ?Shared\\HTTPMetadata $httpMeta; /** * Details about a payment * * @var ?Payment $payment */ public Payment? Payment { get; set; } = default!; } /*--------------------------------*/ class HTTPMetadata { /** * Raw HTTP response; suitable for custom response parsing * * @var \\Psr\\Http\\Message\\ResponseInterface $response */ #[\\Speakeasy\\Serializer\\Annotation\\Exclude] public \\Psr\\Http\\Message\\ResponseInterface $response; /** * Raw HTTP request; suitable for debugging * * @var \\Psr\\Http\\Message\\RequestInterface $request */ #[\\Speakeasy\\Serializer\\Annotation\\Exclude] public \\Psr\\Http\\Message\\RequestInterface $request; }`, }, { label: "Ruby", language: "ruby", code: `class SDK # @param id String # @return GetPaymentResponse def get_payment(string id) end end ##################################### class GetPaymentResponse include Crystalline::MetadataFields field :http_meta, Models::Shared::HTTPMetadata, { 'format_json': { 'letter_case': ::OpenApiSDK::Utils.field_name('-'), required: true } } field :payment, Crystalline::Nilable.new(Shared::Payment) end ##################################### class HTTPMetadata extend T::Sig include Crystalline::MetadataFields # Raw HTTP response; suitable for custom response parsing field :response, ::Faraday::Response, { 'format_json': { 'letter_case': ::OpenApiSDK::Utils.field_name('-'), required: true } } # Raw HTTP request; suitable for debugging field :request, ::Faraday::Request, { 'format_json': { 'letter_case': ::OpenApiSDK::Utils.field_name('-'), required: true } } end` } ]} /> Built-in HTTP metadata is included in both custom and built-in error types that are thrown or returned from the SDK. Of the three response formats, `envelope-http` provides the most details about the underlying HTTP requests but adds a layer of indirection with a wrapper value. #### Accessing HTTP headers with envelope-http When using `envelope-http`, HTTP headers are accessible through the `httpMeta.response.headers` property, providing a cleaner structure than using `rawResponse`: ```typescript const { data } = useEmployeesGetSuspense({ companyId, page: currentPage, per: itemsPerPage, }); console.log(data.httpMeta.response.headers.get("x-total-count")); ``` #### Headers in SDKs Headers in SDKs are treated as metadata rather than structured response objects for several reasons: - Headers are metadata, separate from the main response payload - The dynamic nature of headers makes strict typing impractical - This approach follows standard SDK behavior across the industry While headers can be defined with types in OpenAPI, SDKs generally don't expose them as typed properties. The `envelope-http` format provides a clear separation between HTTP metadata (including headers) and the main response payload. ### `responseFormat: envelope` The `responseFormat: envelope` format builds response types with a wrapper that holds the response value and minimal information about the underlying HTTP response. > Using `envelope-http` instead of `envelope` is recommended as it > provides a more complete view of the HTTP request and response. When `responseFormat: envelope` is enabled, the generated SDK code will produce the response types below: {} } /*--------------------------------*/ export type GetPaymentResponse = { /** * HTTP response content type for this operation */ contentType: string; /** * HTTP response status code for this operation */ statusCode: number; /** * Raw HTTP response; suitable for custom response parsing */ rawResponse: Response; /** * Details about a payment */ payment?: components.Payment | undefined; };`, }, { label: "Python", language: "python", code: `class SDK: def get_payment(self, id: str) -> operations.GetPaymentResponse: #################################### @dataclasses.dataclass class GetPaymentResponse: http_meta: components_httpmetadata.HTTPMetadata = dataclasses.field() payment: Optional[components_payment.Payment] = dataclasses.field(default=None) r"""Details about a payment""" #################################### @dataclasses.dataclass class GetPaymentResponse: content_type: str = dataclasses.field() r"""HTTP response content type for this operation""" status_code: int = dataclasses.field() r"""HTTP response status code for this operation""" raw_response: requests_http.Response = dataclasses.field() r"""Raw HTTP response; suitable for custom response parsing""" payment: Optional[components_payment.Payment] = dataclasses.field(default=None) r"""Details about a payment"""`, }, { label: "Go", language: "go", code: `func (s *SDK) GetPayment(ctx context.Context, id string) (*operations.GetPaymentResponse, error) {} /*--------------------------------*/ type GetPaymentResponse struct { // HTTP response content type for this operation ContentType string // HTTP response status code for this operation StatusCode int // Raw HTTP response; suitable for custom response parsing RawResponse *http.Response // Details about a payment Payment *components.Payment }`, }, { label: "C#", language: "csharp", code: `Task GetPaymentAsync(string id); /*--------------------------------*/ public class GetPaymentResponse { /// /// HTTP response content type for this operation /// public string? ContentType { get; set; } = default!; /// /// HTTP response status code for this operation /// public int StatusCode { get; set; } = default!; /// /// Raw HTTP response; suitable for custom response parsing /// public HttpResponseMessage RawResponse { get; set; } = default!; /// /// OK /// public Payment? Res { get; set; } }`, }, { label: "PHP", language: "php", code: `class SDK { public function getPayment(string $id, ?Options $options = null): GetPaymentResponse } /*--------------------------------*/ class GetPaymentResponse { /** * HTTP response content type for this operation * * @var string $contentType */ public string $contentType; /** * HTTP response status code for this operation * * @var int $statusCode */ public int $statusCode; /** * Raw HTTP response; suitable for custom response parsing * * @var \\Psr\\Http\\Message\\ResponseInterface $rawResponse */ public \\Psr\\Http\\Message\\ResponseInterface $rawResponse; /** * Details about a payment * * @var ?Payment $payment */ public ?Payment $payment = null; }`, }, { label: "Ruby", language: "ruby", code: `class SDK # @params id ::String # @return GetPaymentReponse def get_payment(id) end end ####################################### class GetPaymentResponse include Crystalline::MetadataFields # HTTP response content type for this operation field content_type ::String # HTTP response status code for this operation field status_code ::Integer # Raw HTTP response; suitable for custom response parsing field raw_response ::Faraday::Response # Details about a payment field payment Crystalline::Nilable.new(Models::PaymentPublic) end` } ]} /> # Use Custom HTTP Clients Source: https://speakeasy.com/docs/sdks/customize/runtime/custom-http-client import { CodeWithTabs } from "@/mdx/components"; SDK users can provide a custom HTTP client when initializing SDKs. This is useful for modifying or debugging requests and responses in flight. See below for per-language examples: fetch(request), }); httpClient.addHook("requestError", (err) => { console.log(\`Request failed: \${err}\`); }); // Initialize the SDK with the custom HTTP client const sdk = new SDK({ httpClient });`, }, { label: "Java", language: "java", code: `/* The Java SDK will accept a client that implements the \`HTTPClient\` interface in the \`utils\` package. This will wrap a \`java.net.http.HttpClient\` instance and the call to \`send\`. */ // Custom HTTP client YouHttpClient client = new YourHttpClient(); SDK.Builder builder = SDK.builder(); builder.setClient(client); SDK sdk = builder.build();`, }, { label: "C#", language: "csharp", code: `/* YourHttpClient must implement the ISpeakeasyHttpClient interface */ var httpClient = new YourHttpClient(); // Initialize the SDK with the custom HTTP client var sdk = new SDK(client: httpClient);`, } ]} /> # Enabling JSON lines responses Source: https://speakeasy.com/docs/sdks/customize/runtime/jsonl-events import { CodeWithTabs, Callout } from "@/mdx/components"; **JSON Lines** (JSONL) or **Newline Delimited JSON** (NDJSON) is a simple and efficient format for streaming structured data. Each line in the stream is a valid JSON object, making it ideal for streaming large datasets, log files, or real-time data feeds. This format is particularly useful for processing data line by line without loading the entire response into memory. The format is known by two names and content types, which are completely interchangeable: - JSON Lines (JSONL): `application/jsonl` - Newline Delimited JSON (NDJSON): `application/x-ndjson` Either content type can be used in the OpenAPI specification, as they are functionally identical (each line must be a valid JSON object that matches the specified schema). Here's an example of using an SDK to stream log data in JSONL/NDJSON format: { console.error('Error streaming logs:', error); });`, }, { label: "Python", language: "python", code: `from speakeasy_sdk import SDK with SDK() as sdk: res = sdk.logs.fetch_logs() assert res.object is not None with res.object as jsonl_stream: for event in jsonl_stream: # Handle the event print(event, flush=True)`, }, { label: "Go", language: "go", code: `import( "context" "speakeasy_sdk" "log" ) func main() { ctx := context.Background() s := speakeasy_sdk.New() res, err := s.logs.FetchLogs(ctx) if err != nil { log.Fatal(err) } if res.Object != nil { for res.Object.Next() { event, _ := res.Object.Value() log.Print(event) // Handle the event } } }`, }, { label: "Java", language: "java", code: `import java.lang.Exception; import speakeasy.sdk.SDK; import speakeasy.sdk.models.operations.ReadJsonlResponse; import speakeasy.sdk.models.operations.ReadJsonlResponseBody; import speakeasy.sdk.utils.JsonLStream; public class Application { public static void main(String[] args) throws Exception { SDK sdk = SDK.builder() .build(); ReadJsonlResponse res = sdk.logs().fetch_logs() .call(); // Stream, must be closed after use! try (JsonLStream events = res.events()) { events.stream().forEach(System.out::println); } } }`, } ]} /> The JSONL/NDJSON streaming feature is currently supported in TypeScript, Python, Go, and Java. Let us know if you'd like to see support for other languages. ## Modeling JSONL/NDJSON in OpenAPI To implement line-delimited JSON streaming in generated SDKs, model an API endpoint that serves a stream in the OpenAPI document. Each line in the response will be a JSON object matching the specified schema. Either `application/jsonl` or `application/x-ndjson` can be used as the content type. ### Basic implementation The example below shows an operation that streams log events: ```yaml paths: /logs: get: summary: Stream log events operationId: stream tags: - logs parameters: - name: query in: query required: true schema: type: string responses: "200": description: Log events stream content: # Either content type can be used: application/jsonl: schema: $ref: "#/components/schemas/LogEvent" # OR application/x-ndjson: schema: $ref: "#/components/schemas/LogEvent" components: schemas: LogEvent: description: A log event in line-delimited JSON format type: object properties: timestamp: type: string message: type: string ``` ### Endpoints with multiple response types For APIs that support both batch and streaming responses, use URL fragments to define separate paths for each response type: ```yaml paths: /analytics: get: summary: > Get analytics events as a batch response operationId: getBatch tags: [analytics] parameters: - name: start_date in: query required: true schema: type: string format: date responses: "200": description: Analytics events batch content: application/json: schema: type: array items: $ref: "#/components/schemas/AnalyticsEvent" /analytics#stream: get: summary: > Stream analytics events in real-time operationId: stream tags: [analytics] parameters: - name: start_date in: query required: true schema: type: string format: date responses: "200": description: Analytics events stream content: application/x-ndjson: schema: $ref: "#/components/schemas/AnalyticsEvent" ``` Use the appropriate method based on the requirements: events = streamResponse.events()) { events.stream().forEach(event -> { // Handle the event System.out.println(event); }); } } }`, } ]} /> # Override Accept Headers Source: https://speakeasy.com/docs/sdks/customize/runtime/override-accept-headers import { CodeWithTabs } from "@/mdx/components"; The OpenAPI specification makes it easy to use the `content` directive to specify which endpoints in an API support multiple content types. In this example, our get-all-users endpoint can return a response encoded either as unstructured `text/plain` data or as a structured `application/json` document. ```yaml /getall: get: operationId: getAll tags: - users responses: "200": description: OK content: text/plain: schema: type: string application/json: schema: type: string ``` When invoking the operation normally, the Speakeasy SDK automatically defaults to the first option in the list, in this case, `text/plain`. For any API operations that specify multiple accept headers in the OpenAPI specification, the Speakeasy SDK provides a mechanism to override the accept header to receive data in the preferred format. ## Accept Header Override in Go In Go, all types from all operations are collected into a global `AcceptHeaderEnum` type that can be found in `sdk/operations/options.go`. ```go type AcceptHeaderEnum string const ( AcceptHeaderEnumApplicationJson AcceptHeaderEnum = "application/json" AcceptHeaderEnumTextPlain AcceptHeaderEnum = "text/plain" ) ``` Invoking the `WithAcceptHeaderOverride` function with the appropriate `AcceptHeaderEnum` creates the optional parameter to pass to the operation: ```go s := sdk.New() ctx := context.Background() res, err := s.Users.GetAll(ctx, operations.WithAcceptHeaderOverride(operations.AcceptHeaderEnumApplicationJSON)) ``` ## Accept Header Override in Python and TypeScript In Python and TypeScript, each operation with multiple specified accept headers will have an enum created that provides the acceptable options. The name of the enum will be the tag name, followed by the operation name, followed by `AcceptEnum`. For the example above, that would be `UsersGetAllAcceptEnum`. ## Unspecified Accept Headers While it is strongly recommended to add all accept headers to the OpenAPI spec, in Go, it is possible to override the accept header to an unspecified value. ```go s := sdk.New() ctx := context.Background() res, err := s.Users.GetAll(ctx, operations.WithAcceptHeaderOverride("application/json+debug")) ``` There is no support for unspecified accept headers in Python or TypeScript. # Adding pagination to SDKs Source: https://speakeasy.com/docs/sdks/customize/runtime/pagination Customize pagination rules for each API operation using the `x-speakeasy-pagination` extension. Adding pagination to an SDK enhances the developer experience by providing a structured way to handle paginated API responses. ```python response = sdk.paginatedEndpoint(page=1) while response is not None: # handle response response = response.next() ``` The `next()` function returns `nil, nil` when there are no more pages to retrieve, indicating the end of pagination rather than an error ## Configuring pagination To configure pagination, add the `x-speakeasy-pagination` extension to the OpenAPI description. ```yaml /paginated/endpoint: get: parameters: - name: page in: query schema: type: integer required: true responses: "200": description: OK content: application/json: schema: title: res type: object properties: resultArray: type: array items: type: integer required: - resultArray x-speakeasy-pagination: type: offsetLimit inputs: - name: page in: parameters type: page outputs: results: $.resultArray ``` The `x-speakeasy-pagination` configuration supports `offsetLimit`, `cursor`, and `url` implementations of pagination. ### Offset and limit pagination For `type: offsetLimit pagination`, specify at least one of the following `inputs`: `offset` or `page`. ```yaml x-speakeasy-pagination: type: offsetLimit inputs: - name: page # This input refers to the value called `page` in: parameters # In this case, page is an operation parameter (header, query, or path) type: page # The page parameter will be used as the page-value for pagination, and will be incremented when `next()` is called - name: limit # This input refers to the value called `limit` in: parameters # In this case, limit is an operation parameter (header, query, or path) type: limit # The limit parameter will be used as the limit-value for pagination outputs: results: $.data.resultArray # The data.resultArray value of the response will be used to infer whether there is another page ``` At least one response object must have the following structure: ```json { "data": { "resultArray": [] } } ``` If `inputs.limit` is defined in the pagination configuration, `next()` will return `null` when `$.data.resultArray` has a length of less than the `inputs.limit` value. If `inputs.limit` is omitted, `next()` will return `null` when the length of `$.data.resultArray` is zero. When using the page input, `output.numPages` can be used instead of `output.results` to determine when the pages for the operation are exhausted. ```yaml x-speakeasy-pagination: type: offsetLimit inputs: - name: page # This input refers to the value called `page` in: parameters # In this case, page is an operation parameter (header, query, or path) type: page # The page parameter will be used as the page, and will be incremented when `next()` is called outputs: numPages: $.data.numPages # The data.numPages value of the response will be used to infer whether there is another page ``` If `output.numPages` is provided, `next()` returns `null` when the incremented page number is greater than the `numPages` value. At least one response object must have the following structure: ```json { "data": { "numPages": 1 } } ``` For example, in the following `inputs.offset` configuration, `inputs.limit` has the same effect as in the `inputs.page` example. ```yaml x-speakeasy-pagination: type: offsetLimit inputs: - name: offset # This offset refers to the value called `offset` in: parameters # In this case, offset is an operation parameter (header, query, or path) type: offset # The offset parameter will be used as the offset, which will be incremented by the length of the `output.results` array outputs: results: $.data.resultArray # The length of data.resultArray value of the response will be added to the `offset` value to determine the new offset ``` ### Cursor-based pagination For `type: cursor pagination`, configure the `nextCursor` output. This pagination type uses a cursor value from the previous response. The following is an example `inputs.cursor` configuration. ```yaml x-speakeasy-pagination: type: cursor inputs: - name: since in: requestBody type: cursor outputs: nextCursor: $.data.resultArray[-1].created_at ``` Because the input above is `in` the `requestBody`, this operation must take a request body with **at least** the following structure: ```json { "since": "" } ``` At least one response object must have the following structure: ```json { "data": { "resultArray": [ { "created_at": "" } ] } } ``` The `[-1]` syntax in `outputs.nextCursor` indicates the last value in an array using JSONPath negative indexing. Ensure the type of `requestBody.since` matches the type of `outputs.nextCursor`. ### URL-based pagination When an API returns a URL for the next page, you can use the `url` type in `x-speakeasy-pagination`. Here's an example configuration: ```yaml /paginated/endpoint: get: parameters: - name: page in: query schema: type: integer required: true responses: "200": description: OK content: application/json: schema: title: PaginatedResponse type: object properties: results: type: array items: type: object next: type: string format: uri required: - results - next x-speakeasy-pagination: type: url outputs: nextUrl: $.next ``` The `x-speakeasy-pagination` configuration specifies the type as `url` and uses a JSONPath expression to extract the `nextUrl` from the response. The response object for the URL-based pagination should have the following structure: ```json { "results": [{ "field": "value" }], "next": "http://some_url?page=2" } ``` ## Inputs **`name`** With `in: parameters`, this is the name of the parameter to use as the input value. With `in: requestBody`, this is the name of the request-body property to use as the input value. **`in`** Indicates whether the input should be passed into the operation as a path or query parameter (`in: parameters`) or in the request body (`in: requestBody`). Only simple objects are permitted as values in the request body. **`type`** import { Table } from "@/mdx/components";
page", description: "The variable that will be incremented on calling `next()`.", }, { type: "`offset`", description: "The variable that will be incremented by the number of results returned by the previous execution. **Note:** Requires `outputs.Results`.", }, { type: "`limit`", description: "When provided, `next()` returns `null` (or equivalent) when the number of results returned by the previous execution is less than the value provided.", }, { type: "cursor", description: "The variable that will be populated with the value from `outputs.nextCursor` when calling `next()`. **Note:** Required for `type: cursor` pagination.", }, ]} columns={[ { key: "type", header: "Type" }, { key: "description", header: "Description" }, ]} /> ## Outputs All the outputs are expected to be strings adhering to the [JSONPath](https://goessner.net/articles/JsonPath/) schema.
If the JSONPath value provided for an output does not match the response returned, `next()` returns `null` because pagination cannot be continued. # Adding polling to SDKs Source: https://speakeasy.com/docs/sdks/customize/runtime/polling import { Callout } from "@/mdx/components"; Customize polling methods for each API operation using the `x-speakeasy-polling` extension. Adding polling to an SDK enhances the developer experience by providing a consistent way to handle consuming code that waits for a particular API response change, which is common in API backends that perform asynchronous operations. ```go opts := []operations.Option{ operations.WithPolling(client.PollingEndpointWaitForCompleted()), } response, err := client.PollingEndpoint(ctx, id, opts...) ``` The polling methods, such as `PollingEndpointWaitForCompleted()` above, are generated from configuration within the API operation that declares one or more success criteria. ## Configuring polling To configure polling, add the `x-speakeasy-polling` extension to the OpenAPI Specification document. ```yaml /example/{id}: get: parameters: - name: id in: path schema: type: string required: true responses: "200": description: OK content: application/json: schema: type: object properties: name: type: string status: type: string enum: - completed - errored - pending - running required: - name - status x-speakeasy-polling: - name: WaitForCompleted failureCriteria: - condition: $statusCode == 200 - condition: $response.body#/status == "errored" successCriteria: - condition: $statusCode == 200 - condition: $response.body#/status == "completed" ``` The `x-speakeasy-polling` configuration supports multiple entries with unique names, which will generate unique polling methods with their required success criteria. ## Configuration options ### delaySeconds Customize the delay before the API operation requests begin. By default, this is set to 1 second. In this example: ```yaml x-speakeasy-polling: - name: WaitForCompleted delaySeconds: 5 failureCriteria: - condition: $statusCode == 200 - condition: $response.body#/status == "errored" successCriteria: - condition: $statusCode == 200 - condition: $response.body#/status == "completed" ``` The `WaitForCompleted` polling method makes the first API operation request after 5 seconds. Clients can override this value when using the SDK. ### failureCriteria Customize the polling method to immediately return an error when defined conditions are met. This is useful when the API response contains any sort of "errored" or "failed" status value. It is not necessary to define failure criteria for API responses that are already considered errors, such as a 4XX or 5XX HTTP status code. In this example: ```yaml x-speakeasy-polling: - name: WaitForCompleted failureCriteria: - condition: $statusCode == 200 - condition: $response.body#/status == "errored" successCriteria: - condition: $statusCode == 200 - condition: $response.body#/status == "completed" ``` The `WaitForCompleted` polling method will immediately return an error when the response body `status` property value is `errored`. Clients cannot override this behavior. Refer to `successCriteria` for additional information about supported criteria as they share implementation details. ### intervalSeconds Customize the interval between the API operation requests. By default, this is set to 1 second. In this example: ```yaml x-speakeasy-polling: - name: WaitForCompleted failureCriteria: - condition: $statusCode == 200 - condition: $response.body#/status == "errored" intervalSeconds: 5 successCriteria: - condition: $statusCode == 200 - condition: $response.body#/status == "completed" ``` The `WaitForCompleted` polling method makes the next API operation request 5 seconds after the previous response. Clients can override this value when using the SDK. ### limitCount Customize the total number of API operation requests before raising an error. By default, this is set to 60. In this example: ```yaml x-speakeasy-polling: - name: WaitForCompleted failureCriteria: - condition: $statusCode == 200 - condition: $response.body#/status == "errored" limitCount: 120 successCriteria: - condition: $statusCode == 200 - condition: $response.body#/status == "completed" ``` The `WaitForCompleted` polling method makes at most 120 API operation requests. Clients can override this value when using the SDK. ### successCriteria Configure the polling method success criteria. This configuration is required for all polling methods and is a limited subset of [OpenAPI Arazzo criterion](https://spec.openapis.org/arazzo/latest.html#criterion-object). Each criterion is evaluated in configuration order as a logical AND with prior criterion. Supported criterion contexts include: - `$statusCode`: HTTP status code. Must be defined before response body context. - `$response.body`: HTTP response body data. A value within the data is defined with a JSONPath expression. Supported criterion types include: - `simple`: Assert against a context value using logical operators, such as equals. Default when no `type` is defined. - `regex`: Assert against a context value using a regular expression pattern. #### simple criterion Define context, logical operator, and the value via `condition`. Supported `simple` type logical operators include: - `==`: Equals - `!=`: Not Equals In this example: ```yaml x-speakeasy-polling: - name: WaitForCompleted successCriteria: - condition: $statusCode == 200 - condition: $response.body#/status == "completed" ``` The `WaitForCompleted` polling method waits until both the HTTP status code is 200 and the HTTP response body `status` property is `completed`. Certain language implementations, such as Go, also support using error status codes as success criteria. In these cases any SDK error that would have been returned to the SDK client are swallowed instead. In this example: ```yaml x-speakeasy-polling: - name: WaitForNotFound successCriteria: - condition: $statusCode == 404 ``` The `WaitForNotFound` polling method immediately returns without error when the API operation returns a 404 Not Found HTTP status code. #### regex criterion Define context via `context` and the regular expression pattern via `condition`. The regular expression pattern must be compatible with the target language. For example, the Go language uses the RE2 regular expression engine. In this example: ```yaml x-speakeasy-polling: - name: WaitForCompleted successCriteria: - condition: $statusCode == 200 - context: $response.body#/status condition: "^(completed|ready)$" type: regex ``` The `WaitForCompleted` polling method waits until both the HTTP status code is 200 and the HTTP response body `status` property is either `completed` or `ready`. # Retries Source: https://speakeasy.com/docs/sdks/customize/runtime/retries import { CodeWithTabs, Table } from "@/mdx/components"; With Speakeasy, generate SDKs that automatically retry requests that fail due to network errors or any configured HTTP status code. Enable retries globally for all requests or on a per-request basis by using the `x-speakeasy-retries` extension in the OpenAPI document. The SDK end user can then override the default configuration by passing in a `RetryConfig` object to the operation at runtime. ## Global Retries ```yaml openapi: 3.0.3 info: title: Swagger Petstore - OpenAPI 3.0 version: 1.0.11 servers: - url: https://petstore3.swagger.io/api/v3 x-speakeasy-retries: strategy: backoff backoff: initialInterval: 500 # 500 milliseconds maxInterval: 60000 # 60 seconds maxElapsedTime: 3600000 # 1 hour exponent: 1.5 statusCodes: - 5XX retryConnectionErrors: true ``` If you add the `x-speakeasy-retries` extension to the root of the OpenAPI document, the SDK Generator will generate a global retry configuration that will be used for all requests made by the SDK. The `x-speakeasy-retries` extension supports the following properties:
The `statusCodes` property supports passing a list of distinct HTTP status codes or a wildcard range to retry requests on. For example, `5XX` will retry requests on all status codes between 500 (inclusive) and 600 (exclusive). Set the `retryConnectionErrors` property to `true` to configure retrying requests that fail due to network errors like DNS resolution errors or connection refused errors. The defaults for the backoff strategy are: - `initialInterval`: 500 - `maxInterval`: 60000 - `maxElapsedTime`: 3600000 - `exponent`: 1.5 ## Per-request Retries Add the `x-speakeasy-retries` extension to any operation to override the global retry configuration for that operation, or to configure retries for the operation if retries aren't defined globally. For example, you might want to configure retries for an operation on a different set of HTTP status codes or specify different intervals between retries. ```yaml openapi: 3.0.3 info: title: Swagger Petstore - OpenAPI 3.0 version: 1.0.11 servers: - url: https://petstore3.swagger.io/api/v3 paths: /pet/findByStatus: get: operationId: findPetsByStatus x-speakeasy-retries: strategy: backoff backoff: initialInterval: 500 # 500 milliseconds maxInterval: 60000 # 60 seconds maxElapsedTime: 3600000 # 1 hour exponent: 1.5 statusCodes: - 408 - 500 - 502 - 503 retryConnectionErrors: true parameters: - name: status in: query description: Status values that need to be considered for filter required: false explode: true schema: type: string default: available enum: - available - pending - sold responses: "200": description: successful operation content: application/json: schema: type: array items: $ref: "#/components/schemas/Pet" application/xml: schema: type: array items: $ref: "#/components/schemas/Pet" "400": description: Invalid status value ``` Per-request retries are configured the same way as global retries. ## SDK Usage Override Users of the SDK can override the retry strategy globally or at the request level by providing a `utils.RetryConfig` object. This object supports most of the same properties as the `x-speakeasy-retries` extension, except for the status codes to retry on. ### Global To override the retry strategy globally, initialize the SDK object with the appropriate options parameter. In all cases, if no override is provided, the retry configuration provided in the OpenAPI document will be used. In this example, we use the `RetryConfig` object to disable retries globally: setRetryConfig(new Retry\\RetryConfigNone())->build();`, }, ]} /> ### Request-Level Override Any endpoints that support retries allow retry configuration to be overridden. In Go, override request-level retry configuration with `operations.WithRetries`. In Python and TypeScript, the optional `retries` is accepted. .SDK; import .models.operations.FindPetsByStatusResponse SDK s = SDK.builder().build(); FindPetsByStatusResponse res = sdk.findPetsByStatus() .retryConfig(RetryConfig.builder() .backoff(BackoffStrategy.builder() .initialInterval(100L, TimeUnit.MILLISECONDS) .maxInterval(10000L, TimeUnit.MILLISECONDS) .maxElapsedTime(60000L, TimeUnit.MILLISECONDS) .exponent(1.1f) .retryConnectError(false) .build()) .build()) .call();`, }, { label: "C#", language: "csharp", code: `var sdk = new SDK(); var res = await sdk.FindPetsByStatusAsync(retryConfig: new RetryConfig( strategy: RetryConfig.RetryStrategy.BACKOFF, backoff: new BackoffStrategy( initialIntervalMs: 100L, maxIntervalMs: 10000L, maxElapsedTimeMs: 60000L, exponent: 1.1 ), retryConnectionErrors: false ));`, }, { label: "PHP", language: "php", code: `$sdk = SDK::builder()->build(); $responses = $sdk->findPetsByStatus( options: Utils\\Options->builder()->setRetryConfig( new Retry\\RetryConfigBackoff( initialInterval: 100, maxInterval: 10000, exponent: 1.1, maxElapsedTime: 60000, retryConnectionErrors: false, ))->build());`, }, { label: "Ruby", language: "ruby", code: `require 'openapi' Models = ::OpenApiSDK::Models s = ::OpenApiSDK::SDK.new responses = s.find_pets_by_status( retries: BackoffStrategy.new( initialInterval: 100, maxInterval: 10000, exponent: 1.1, maxElapsedTime: 60000, retryConnectionErrors: false, ));`, } ]} /> ## Idempotency For endpoints with a defined idempotency header, retries use the exact idempotency key provided for the initial request. ```yaml paths: /pet: put: operationId: putPet x-speakeasy-retries: strategy: backoff backoff: initialInterval: 500 maxInterval: 60000 maxElapsedTime: 3600000 exponent: 1.5 statusCodes: - 5XX retryConnectionErrors: false parameters: - name: Idempotency-Key schema: type: string in: header requestBody: content: application/json: schema: $ref: "#/components/schemas/Pet" responses: "200": description: successful operation content: application/json: schema: $ref: "#/components/schemas/Pet" default: description: Error ``` ## Respecting `Retry-After` If an API returns an HTTP standard `retry-after` header, the SDK will respect that value as the retry interval as long as the time is in the future and below the max elapsed retry time. No configuration changes are needed to enable this; simply return a `retry-after` header from the API. This feature is currently supported in TypeScript with support for additional languages coming in the future. ```yaml responses: "429": description: Too Many Requests headers: Retry-After: description: The date and time after which the client can retry the request. schema: type: string format: date-time example: "Wed, 21 Oct 2023 07:28:00 GMT" ``` ```yaml responses: "429": description: Too Many Requests headers: Retry-After: description: The number of seconds to wait before retrying the request. schema: type: integer example: 120 ``` # Enabling Event-Streaming Operations Source: https://speakeasy.com/docs/sdks/customize/runtime/server-sent-events import { Callout } from "@/mdx/components"; Server-sent events (SSE) is a core web feature that provides servers with a low overhead solution to push real-time events to the client when they become available. SSE can be used to stream chat completions from a large language model, real-time stock prices, and sensor readings to clients. SSE is similar to WebSockets in that it uses a persistent connection but differs in that it is unidirectional - only the server sends events. SSE is simpler to implement in many existing backend HTTP frameworks. Speakeasy makes it easy to build SSE into generated SDKs without vendor extensions or heuristics. Leverage SSE by modeling SSE streams as `text/event-stream` responses with pure OpenAPI. Here's a short example of using an SDK to chat with an LLM and read its response as a stream: ```typescript import { SDK } from '@speakeasy/sdk'; const sdk = new SDK() const response = await sdk.chat.create({ prompt: "What are the top 3 French cheeses by consumption?" }) for await (const event of response.chatStream) { process.stdout.write(event.data); } ``` The SSE feature is currently supported in TypeScript, Python, Go, and Java. Let us know if you'd like to see support for other languages. ## Modeling SSE in OpenAPI To implement SSE in a generated SDKs, model an API endpoint that serves an event stream in an OpenAPI document. **Each server-sent event can contain up to four types of fields:** `id`, `event`, `data`, and `retry`. ### Basic Implementation The example below illustrates an operation that streams events containing only a `data` field that holds string content: ```yaml paths: /chat: post: summary: Create a chat completion from a prompt operationId: create tags: [chat] requestBody: required: true content: application/json: schema: $ref: "#/components/schemas/ChatRequest" responses: "200": description: Chat completion created content: text/event-stream: schema: $ref: "#/components/schemas/ChatStream" components: schemas: ChatRequest: type: object required: [prompt] properties: prompt: type: string ChatStream: description: A server-sent event containing chat completion content type: object required: [data] properties: data: type: string ``` ### When `data` is a JSON Object SSE implementation isn't limited to string data. If `data` is specified as an object instead of a string, then SDKs will assume the field will contain JSON content. Raw data received from the server will be deserialized into an object for the application code to consume. ```yaml components: schemas: ChatStream: description: A server-sent event containing chat completion content type: object required: [data] properties: data: type: object properties: content: type: string model: type: string enum: ["foo-gpt-tiny", "foo-gpt-small"] created: type: integer ``` The Speakeasy-generated TypeScript SDK for the example above will allow users to access this object: ```typescript for await (const event of response.chatStream) { const { content, model, created } = event.data; process.stdout.write(content); } ``` ### Handling Multiple Event Types Other streaming APIs send multiple types of events with the `id` and `event` fields. These event types can be described as a union (`oneOf`) with the `event` field acting as a discriminator: ```yaml components: schemas: ChatStream: oneOf: - $ref: "#/components/schemas/HeartbeatEvent" - $ref: "#/components/schemas/ChatEvent" discriminator: propertyName: event mapping: ping: "#/components/schemas/HeartbeatEvent" completion: "#/components/schemas/ChatEvent" HeartbeatEvent: description: A server-sent event indicating that the server is still processing the request type: object required: [event] properties: event: type: string const: "ping" ChatEvent: description: A server-sent event containing chat completion content type: object required: [id, event, data] properties: id: type: string event: type: string const: completion data: type: object required: [content] properties: content: type: string ``` ### Endpoints with Multiple Response Types For APIs that handle both JSON responses and streaming events, use **URL fragments** to define separate paths for each response type. Each fragment maps to a specific behavior—either returning a complete JSON response or streaming data. This approach allows Speakeasy to generate distinct SDK methods with clear return types while maintaining API flexibility. ```yaml paths: /chat: post: summary: > Create a chat completion from a prompt. The entire response is returned as a single JSON object. operationId: create tags: [chat] requestBody: required: true content: application/json: schema: $ref: "#/components/schemas/ChatRequestJson" responses: "200": description: Chat completion created content: application/json: schema: $ref: "#/components/schemas/ChatResponse" /chat#streamed: post: summary: > Create a chat completion from a prompt. The response is streamed in chunks as it is generated. operationId: createStreamed tags: [chat] requestBody: required: true content: application/json: schema: $ref: "#/components/schemas/ChatRequestStream" responses: "200": description: Chat completion created content: text/event-stream: schema: $ref: "#/components/schemas/ChatStream" components: schemas: ChatRequest: # ... ChatRequestJson: allOf: - $ref: "#/components/schemas/ChatRequest" - type: object properties: # !mark(1:4) stream: type: boolean enum: [false] default: false ChatRequestStream: allOf: - $ref: "#/components/schemas/ChatRequest" - type: object properties: # !mark(1:4) stream: type: boolean enum: [true] default: true ChatResponse: # ... ChatStream: # ... ``` The `stream` properties in the `ChatRequestJson` and `ChatRequestStream` schemas are treated as constants, ensuring that each request type always has a fixed stream value (false for JSON responses and true for streamed responses). In OpenAPI 3.0, this is achieved using single-value enums. For OpenAPI 3.1, simplify schema by using the `const` field instead of `enum`, which explicitly defines the property as having a constant value. This makes the specification more concise and easier to maintain. See the [Speakeasy OpenAPI reference on enums](/openapi/schemas/enums) for more information. Use `chat` for the non-streaming endpoint and `chatStreamed` for the streaming endpoint: ```typescript import { SDK } from '@speakeasy/sdk'; const sdk = new SDK() // Non-streaming method const jsonResponse = await sdk.chat.create({ prompt: "What are the top 3 French cheeses by consumption?" }); console.log(jsonResponse.content); // Streaming method const stream = await sdk.chat.createStreamed({ prompt: "What are the top 3 French cheeses by consumption?" }); for await (const event of response.chatStream) { process.stdout.write(event.data); } ``` #### Alternative: Single method with overloads Instead of using URL fragments to create separate methods, you can define a single endpoint that supports both response types. Since `inferSSEOverload` defaults to `true` in your `gen.yaml`, TypeScript and Python SDKs will automatically generate a single overloaded method that provides type safety based on the `stream` parameter value: The `inferSSEOverload` feature is currently supported in TypeScript and Python, with support for other languages planned for future releases. ```yaml paths: /chat: post: summary: Create a chat completion from a prompt operationId: create tags: [chat] requestBody: required: true content: application/json: schema: $ref: "#/components/schemas/ChatRequest" responses: "200": description: Chat completion response content: application/json: schema: $ref: "#/components/schemas/ChatResponse" text/event-stream: schema: $ref: "#/components/schemas/ChatStream" components: schemas: ChatRequest: type: object required: [prompt] properties: prompt: type: string stream: type: boolean description: Whether to stream the response # The presence of this boolean 'stream' field in the request body # is critical for inferSSEOverload to generate method overloads ChatResponse: # JSON response schema ChatStream: # SSE event schema ``` This generates type-safe methods in both TypeScript and Python: ```typescript import { SDK } from '@speakeasy/sdk'; const sdk = new SDK(); // Non-streaming - returns ChatResponse const response = await sdk.chat.create({ prompt: "Hello", stream: false }); console.log(response.content); // Streaming - returns AsyncIterable const stream = await sdk.chat.create({ prompt: "Hello", stream: true }); for await (const event of stream) { process.stdout.write(event.data); } ``` ```python from myapi import SDK sdk = SDK() # Non-streaming - returns ChatResponse response = sdk.chat.create(prompt="Hello", stream=False) print(response.content) # Streaming - returns Iterator[ChatStream] stream = sdk.chat.create(prompt="Hello", stream=True) for event in stream: print(event.data) ``` Across all of these examples, the schema for the events only ever specifies one or more of the four recognized fields. Adding other fields will trigger a validation error when generating an SDK with the Speakeasy CLI or GitHub action. ## Sentinel events Some SSE APIs will terminate the stream by sending a final, special event. This sentinel event is only used to signal that there are no more events and is not intended for application code to handle. In the example below, the final `data: [DONE]` event is the sentinel event: ``` HTTP/1.1 200 OK Content-Type: text/event-stream; charset=utf-8 Date: Fri, 12 Jul 2024 14:29:22 GMT Keep-Alive: timeout=5, max=1000 Connection: Keep-Alive data: {"content": "there"} data: {"content": "are 7"} data: {"content": "continents in the world"} data: [DONE] ``` To hide this final event in generated SDK methods, use the `x-speakeasy-sse-sentinel: ` extension on a `text/event-stream` media object: ```diff paths: /chat: post: summary: Create a chat completion from a prompt operationId: create tags: [chat] requestBody: required: true content: application/json: schema: $ref: '#/components/schemas/ChatRequest' responses: '200': description: Chat completion created content: text/event-stream: + x-speakeasy-sse-sentinel: '[DONE]' schema: $ref: '#/components/schemas/ChatEvent' components: schemas: ChatEvent: description: A server-sent event containing chat completion content type: object required: [data] properties: data: type: object required: [content] properties: content: type: string ``` Application code like the following TypeScript sample will behave as expected. The async iteration loop will finish when the sentinel event is encountered: ```ts const llm = new LLM(); const stream = await llm.chat.create({ prompt: "How many continents are there?", }); for await (const event of stream) { // ^? ChatEvent process.stdout.write(event.data.content); } ``` # Enabling file streaming operations Source: https://speakeasy.com/docs/sdks/customize/runtime/streaming import { CodeWithTabs } from "@/mdx/components"; Support for streaming is critical for applications that need to send or receive large amounts of data between client and server without first buffering the data into memory, potentially exhausting this system resource. ## Streaming download Creating an endpoint with a top-level binary response body allows treating that response as a streamable, and iterating over it without loading the entire response into memory. This is useful for large file downloads, long-running streaming responses, and more. In an OpenAPI document, this can be modeled as a binary stream. Here's an example of a `get` operation with content type as `application/octet-stream`. ```yaml /streamable: get: operationId: streamable responses: "200": description: OK content: application/octet-stream: schema: title: bytes type: string format: binary ``` ## Streaming uploads Streaming is useful when uploading large files. Certain SDK methods will be generated that accept files as part of a multipart request. It is possible (and recommended) to upload files as a stream rather than reading the entire contents into memory. This avoids excessive memory consumption and potentially crashing with out-of-memory errors when working with large files. In this example, a request to upload a file is managed as a `multipart/form-data` request. ```yaml /file: post: summary: Upload file operationId: upload requestBody: content: multipart/form-data: schema: $ref: "#/components/schemas/UploadFileRequest" required: true responses: "200": description: "" headers: Action-Id: required: true schema: type: string content: application/json: schema: $ref: "#/components/schemas/File" ``` \`, and the SDK call looks like the sample code below. */ import { openAsBlob } from "node:fs"; import { SDK } from "@speakeasy/super-sdk"; async function run() { const sdk = new SDK(); const fileHandle = await openAsBlob("./src/sample.txt"); const result = await sdk.upload({ file: fileHandle }); console.log(result); } run(); /* Depending on the JavaScript runtime, convenient utilities can return a handle to a file without reading the entire contents into memory: */ /** * - **Node.js v20+:** Since version 20, Node.js comes with a native * \`openAsBlob\` function in [\`node:fs\`] (https://nodejs.org/docs/ * latest-v20.x/api/fs.html#fsopenasblobpath-options). * - **Bun:** The native [\`Bun.file\`](https://bun.sh/docs/api/file-io * #reading-files-bun-file) function produces a file handle that can * be used for streaming file uploads. * - **Browsers:** All supported browsers return an instance to a [\`File\`] * (https://developer.mozilla.org/en-US/docs/Web/API/File) when * reading the value from an \`\` element. * - **Node.js v18:** A file stream can be created using the \`fileFrom\` * helper from [\`fetch-blob/from.js\`] * (https://www.npmjs.com/package/fetch-blob). */`, }, { label: "Go", language: "go", code: `/* Use any [\`io.Reader\`](https://pkg.go.dev/io#Reader) implementation, such as calling [\`os.Open()\`] (https://pkg.go.dev/os#Open) on an existing file. */ ctx := context.Background() s := sdk.New() file, err := os.Open("./src/sample.txt") if err != nil { // ... error handling ... } response, err := s.upload(ctx, file)`, } ]} /> # Timeouts Source: https://speakeasy.com/docs/sdks/customize/runtime/timeouts import { CodeWithTabs } from "@/mdx/components"; Speakeasy allows configuring request timeouts in an SDK using the `x-speakeasy-timeout` extension in the OpenAPI document. Timeouts can be enabled globally for all operations or on a per-operation basis. SDK end users can override the default configuration by passing in a timeout option at runtime. Timeout values are always provided in milliseconds. ## Global timeouts ```yaml openapi: 3.0.3 info: title: Swagger Petstore - OpenAPI 3.0 version: 1.0.11 servers: - url: https://petstore3.swagger.io/api/v3 x-speakeasy-timeout: 1000 ``` Adding the `x-speakeasy-timeout` extension to the root of the OpenAPI document configures a global timeout for all requests made by the SDK. ## Per-request timeouts Adding the `x-speakeasy-timeout` extension to any operation overrides the global timeout configuration for that operation or sets a timeout if no global configuration exists. ```yaml openapi: 3.0.3 info: title: Swagger Petstore - OpenAPI 3.0 version: 1.0.11 servers: - url: https://petstore3.swagger.io/api/v3 paths: /pet/findByStatus: get: operationId: findPetsByStatus x-speakeasy-timeout: 2000 parameters: - name: status in: query description: Status values that need to be considered for filter required: false explode: true schema: type: string default: available enum: - available - pending - sold responses: "200": description: successful operation content: application/json: schema: type: array items: $ref: "#/components/schemas/Pet" application/xml: schema: type: array items: $ref: "#/components/schemas/Pet" "400": description: Invalid status value ``` ## Overriding timeout configuration Users of the SDK can override the timeout configuration globally or at the request level. ### Globally overriding timeout configuration To override the timeout configuration globally, initialize the SDK object with the appropriate options parameter. In all cases, if no override is provided, the timeout configuration provided in the OpenAPI document will be used. ### Overriding timeout configuration at the request level Users can override the timeout config on a per-operation basis. # Configure Servers Source: https://speakeasy.com/docs/sdks/customize/servers import { CodeWithTabs, Callout } from "@/mdx/components"; ## Default Behavior The OpenAPI specification allows you to define an array of servers that can be used to make requests to the API. These servers are generally used to define different environments (for example, production, development, and testing) available for the API. ```yaml openapi: 3.0.3 info: title: Example version: 0.0.1 servers: - url: https://prod.example.com # Used as the default URL by the SDK description: Our production environment - url: https://sandbox.example.com description: Our sandbox environment ``` The Speakeasy SDK Generator automatically selects the first server URL from the OpenAPI document's servers list as the default endpoint. While this default is commonly set to the production server, it's flexible to accommodate application development cycles by reordering or modifying the server list. ## Declare Base Server URL Speakeasy SDKs are battery-included, meaning they are designed to work out of the box with minimal configuration from end users. If the OpenAPI document lacks server definitions (both at the global level and for individual operations) or relies on relative paths for server URLs, it's essential to set a default server endpoint. Set the default server endpoint by specifying a `baseServerUrl` in the SDK Generator configuration file (`gen.yaml`). This ensures the SDK always has a primary server to connect to for its operations. ```yaml # ... generation: baseServerUrl: "https://prod.example.com" ``` ## Use Templated URLs [Templated](https://spec.openapis.org/oas/v3.0.3#server-object) URLs provide a dynamic method to customize server endpoints based on runtime parameters, making them ideal for applications that serve multiple clients or operate in varied environments. ```yaml servers: - url: https://{customer}.yourdomain.com variables: customer: default: api description: The name of the customer sending API requests. ``` These placeholders can then be replaced with specific values at runtime, allowing for customer-specific or environment-specific configurations without altering the SDK. The templating feature is only supported for global server URLs and is not yet supported for per-operation server URLs. ## Managing Multiple Servers With IDs For a better developer experience, define an ID for each server using the `x-speakeasy-server-id` extension. This simplifies the process of selecting between servers at SDK initialization. ```yaml openapi: 3.0.3 info: title: Example version: 0.0.1 servers: - url: https://prod.example.com # Used as the default URL by the SDK description: Our production environment x-speakeasy-server-id: prod - url: https://sandbox.example.com description: Our sandbox environment x-speakeasy-server-id: sandbox ``` ## Dynamic Server Declaration at Runtime Dynamic server selection allows developers to switch between multiple predefined servers at runtime, offering flexibility across different deployment environments or client configurations. The Speakeasy README file accompanying the generated SDK includes SDK-specific examples to guide through the process of dynamically selecting servers. ### Methods #### Server Selection by Index Specify a server from the predefined list based on its index. withServerIndex(1)->build();`, }, { label: "Ruby", language: "ruby", code: `sdk = ::OpenApiSDK::SDK.new(server_idx: 1)`, }, ]} /> #### Global URL Override Set a global server URL at SDK initialization, overriding the base URL. // if the x-speakeasy-server-id extension is not used withServerUrl("https://sandbox.example.com") // with x-speakeasy-server-id extension withServer("sandbox")->build();`, }, { label: "Ruby", language: "ruby", code: `sdk = OpenApiSDK::SDK.new( server_url: "https://sandbox.example.com", server: "sandbox" )`, }, ]} /> #### Per-Client or Per-Operation Override Override the server URL for specific instances or API calls. If you choose to configure the SDK URL at runtime and relative paths were used in the OpenAPI document, make sure that you account for the `baseURL` when initializing the SDK server configuration. # Customize imports Source: https://speakeasy.com/docs/sdks/customize/structure/imports import { Callout, CodeWithTabs } from "@/mdx/components"; Speakeasy allows customization of the paths for generated models and model imports. By default, Speakeasy uses an OpenAPI-based naming scheme for the namespaces models are bucketed into, for example: Currently only supported for C#, Go, Python, TypeScript, and Unity SDKs. More languages will be added soon. ### Components Models generated from components are placed in the `models/components` directory, or the target language idiomatic equivalent. ```yaml sdk/ ├─ models/ │ ├─ components/ │ │ ├─ user.ts │ │ ├─ drink.ts │ │ └─ ... │ ├─ operations/ │ │ ├─ getuser.ts │ │ ├─ updateuser.ts │ │ ├─ getdrink.ts │ │ ├─ updatedrink.ts │ │ └─ ... │ └─ errors/ │ ├─ sdkerror.ts │ ├─ responseerror.ts │ └─ ... └─ ... ``` ### Operations Models generated from operations are placed in the `models/operations` directory, or the target language idiomatic equivalent. ```yaml sdk/ ├─ models/ │ ├─ components/ │ │ ├─ user.ts │ │ ├─ drink.ts │ │ └─ ... │ ├─ operations/ │ │ ├─ getuser.ts │ │ ├─ updateuser.ts │ │ ├─ getdrink.ts │ │ ├─ updatedrink.ts │ │ └─ ... │ └─ errors/ │ ├─ sdkerror.ts │ ├─ responseerror.ts │ └─ ... └─ ... ``` ### Errors Models that are used in error status codes are placed in the `models/errors` directory (or the idiomatic equivalent for the target language). ```yaml sdk/ ├─ models/ │ ├─ components/ │ │ ├─ user.ts │ │ ├─ drink.ts │ │ └─ ... │ ├─ operations/ │ │ ├─ getuser.ts │ │ ├─ updateuser.ts │ │ ├─ getdrink.ts │ │ ├─ updatedrink.ts │ │ └─ ... │ └─ errors/ │ ├─ sdkerror.ts │ ├─ responseerror.ts │ └─ ... └─ ... ``` The default names for the model directories are consistent across most target languages, but C# makes the exception that the `models/Operations` directory is called `models/Requests` by default. ## Customize import paths ### imports Customize where path models are generated to and imported from by modifying the configuration in the `gen.yaml` file. Configuration like what is shown will result in a file structure as above. ```yaml configVersion: 2.0.0 generation: # ... typescript: version: 1.0.0 imports: option: openapi paths: callbacks: models/callbacks errors: models/errors operations: models/operations shared: models/components webhooks: models/webhooks # ... ``` ### option The `option` key determines the type of bucketing scheme that is used for the models. Only `openapi` is currently supported. This will bucket models into `components`, `operations`, `errors`, `callbacks`, and `webhooks` directories. ```yaml configVersion: 2.0.0 generation: # ... typescript: version: 1.0.0 imports: option: openapi paths: callbacks: models/callbacks errors: models/errors operations: models/operations shared: models/components webhooks: models/webhooks # ... ``` ### paths The `paths` section contains a map of bucket names to paths relative to the root of the generated SDK. - `shared` refers to the models generated from the `components` section of the OpenAPI specification. (Note: `shared` is a legacy name for the bucket, retained for backward compatibility.) - `operations` refers to the models generated for the request and responses of operations in the OpenAPI specification. - `errors` refers to the models generated for schemas referenced in error status codes responses. - `callbacks` refers to models generated for schemas within the `callbacks` section of an operation. - `webhooks` refers to models generated from the `webhooks` section of an OpenAPI 3.1 document. ```yaml configVersion: 2.0.0 generation: # ... typescript: version: 1.0.0 imports: option: openapi paths: callbacks: models/callbacks errors: models/errors operations: models/operations shared: models/components webhooks: models/webhooks # ... ``` You can customize these paths to any path that exists relative to the root of the SDK. If you are providing custom path names, make sure there is no conflict with any of the existing directories in the SDK. Conflicts will result in compilation issues. Different buckets can also be configured to use the same path, for example: ```yaml filename="gen.yaml" typescript: ... imports: option: openapi paths: callbacks: models errors: models operations: models shared: models webhooks: models ``` This will result in all models being generated to the `models` directory. The generator will then resolve any class name conflicts by prefixing or suffixing class names to ensure they are unique. ## Customize global imports You can configure the generator to work with a global import path for all models. For example: ```typescript import { User, GetDrinkRequest, SDK } from "@speakeasy/bar"; ``` Instead of: ```typescript import { SDK } from "@speakeasy/bar"; import { User } from "@speakeasy/bar/dist/models/components/user"; import { GetDrinkRequest } from "@speakeasy/bar/dist/models/operations/user"; ``` You will configure global imports slightly differently for different languages:

Global imports will cause namespace pollution for the import and file clutter in the directory models are generated to.
Large APIs containing many models (especially many inline models) will inevitably lead to name conflicts. Rename types verbosely to ensure each class is unique within the namespace.
# Customize Namespaces Source: https://speakeasy.com/docs/sdks/customize/structure/namespaces import { CodeWithTabs } from "@/mdx/components"; When exposing an API to users, grouping API methods into namespaces creates an object-oriented SDK interface. This type of interface helps users better conceptualize the objects they are manipulating when using the API. ## Default Behavior By default, Speakeasy uses the `tags` in the OpenAPI spec as organizing principles for namespaces. For each `tag` in the spec, Speakeasy creates a namespace. Each method will then be added to namespaces corresponding with its `tags`. If a method does not have an associated `tag`, it will be added to the root SDK class of the generated client library. If a method has multiple tags associated with it, the operation will appear as a method in multiple classes. The following example illustrates the method `sdk.Drinks.ListDrinks()` assigned to the `Drinks` namespace and another method `sdk.ListLocations()` kept in the default class: ```yaml filename="Tags" paths: /bar_locations: get: operationId: listLocations summary: List all locations of the Speakeasy bar description: Get a list of all the bars being run by Speakeasy responses: "200": description: A list of bars content: application/json: schema: type: array items: $ref: "#/components/schemas/BarLocation" /drinks: get: operationId: listDrinks summary: List all drinks description: Get a list of all drinks served by the bar tags: - drinks responses: "200": description: A list of drinks content: application/json: schema: type: array items: $ref: "#/components/schemas/Drink" tags: - name: drinks description: Everything about our Drinks on offer ``` The generated SDK will include methods that can be invoked as follows: ```go // Method added into the Drinks namespace sdk.Drinks.ListDrinks() // Default method sdk.ListLocations() ``` ## Define Namespaces Without Tags `x-speakeasy-group` Sometimes the `tags` in an OpenAPI spec may already be used for an unrelated purpose (for example, autogenerating documentation). In this scenario, using something other than `tags` to organize methods may be necessary. The `x-speakeasy-group` field allows defining custom namespaces. Add this field to any operation in the OpenAPI spec to override any `tags` associated with that method. For example: ```yaml filename="x-speakeasy-group" paths: /drinks/{drink_type}/get_vintage: get: operationId: getVintage summary: Check the vintage of the wine description: Get the vintage of a drink served by the bar parameters: - name: drink_type in: path description: The type of drink required: true schema: type: string tags: - drinks x-speakeasy-group: wine responses: "200": description: A list of drinks content: application/json: schema: type: array items: $ref: "#/components/schemas/Drink" tags: - name: drinks description: Everything about Drinks on offer ``` The generated SDK will include a method, which can be invoked as follows: ```go // GetVintage - get the vintage of the wine sdk.wine.GetVintage("wine") ``` ## Define Multi-Level Namespaces Use `tags` or the `x-speakeasy-group` extension to define nested namespaces for operations using `.` notation. There is no limit to the number of levels that can be defined. The generated SDK will include a method, invoked as follows: ```go // Get the Vintage of a specified wine. sdk.Drinks.Wine.GetVintage("wine") ``` ## Multiple Namespaces If you want to add a method to multiple namespaces, list multiple values in `tags` or the `x-speakeasy-group` extension. Both accept an array of values: The generated SDK will include methods that can be invoked as follows: ```go // Get the Vintage sdk.Drinks.GetVintage("wine") sdk.Wine.GetVintage("wine") ``` # Additional index exports Source: https://speakeasy.com/docs/sdks/customize/typescript/additional-index-exports To export additional modules, such as utilities and constants, from the main `index.ts` file of the SDK: As Speakeasy generates the contents of `src/index.ts`, do not edit that file directly. Instead, create an `index.extras.ts` file in the `src/` directory to define additional exports from the index file (`src/index.ts`) of the SDK. If an `index.extras.ts` file is present, the SDK generator will automatically re-export its contents from the main `index.ts` file: ```ts filename="/src/index.ts" // { Speakeasy generated exports } export * from "./index.extras.ts"; ``` # Configuring module format Source: https://speakeasy.com/docs/sdks/customize/typescript/configuring-module-format import { Callout } from "@/mdx/components"; Modern SDKs need to balance compatibility with performance. The `moduleFormat` option in the SDK generator allows developers to control whether an SDK is built for CommonJS (CJS), ECMAScript Modules (ESM), or both. This choice impacts bundle size, tree-shaking performance, and compatibility with Node.js and modern bundlers. ## How to configure module format To configure the module format, update the `typescript` section of your `gen.yaml` file (which is often located in the SDK's `.speakeasy` directory): ```yaml filename="/.speakeasy/gen.yaml" typescript: # add or modify `moduleFormat` moduleFormat: "commonjs" # or "esm" or "dual" # other Typescript configuration options... ``` ### Supported options Select one of the supported module formats: - `"commonjs"` is the default option. It builds SDKs for CommonJS. CJS is widely supported across Node.js environments, but it's less optimized for modern bundlers and tree-shaking. - `"esm"` is the modern standard for JavaScript modules. It builds SDKs for ECMAScript Modules. ESM provides optimal tree-shaking and significantly smaller bundles when used with bundlers like Webpack, Rollup, or Vite. - `"dual"` provides the best of both worlds. By building SDKs for both CJS and ESM formats, it offers ESM's superior tree-shaking and bundle optimization while maintaining compatibility with older CJS environments. The slight build time increase is often worth the flexibility and performance benefits. ## Module format overview The `moduleFormat` determines the module system targeted during SDK building. It impacts: - Node.js project compatibility - Bundler tree-shaking capabilities - SDK bundle size - Build performance ### Example outputs Review the different outputs generated for each module format. #### CJS The `commonjs` module format outputs the following: ```javascript filename="example.js" // CommonJS import in consumer code const { ApiError } = require("petstore/errors/apierror.js"); // ESM import (interop code included) import { ApiError } from "petstore/errors/apierror.js"; ``` #### ESM The `esm` module format outputs the following: ```javascript filename="example.js" // Native ESM import in consumer code import { ApiError } from "petstore/errors/apierror.js"; // ❌ Will not work in CommonJS-only environments ``` #### Dual The `dual` module format outputs the following: ```javascript filename="example.js" // ESM import (no interop code) import { ApiError } from "petstore/errors/apierror.js"; // CommonJS import (still works seamlessly) const { ApiError } = require("petstore/errors/apierror.js"); ``` ## How to decide which format to use We recommend using CJS (`commonjs`) if: - The SDK is used primarily in Node.js environments or older projects. - Bundle size optimization is not a critical requirement. - You require maximum compatibility with legacy systems. We recommend using ESM (`esm`) if: - The SDK consumers use modern bundlers like Vite, Webpack, or Rollup. - Tree-shaking and bundle size optimization are top priorities. - The project already uses ESM throughout. - Leveraging the latest JavaScript features and tooling is important. We recommend using both (`dual`) if: - You require support for both modern and legacy environments. - You need ESM's superior tree-shaking while maintaining CJS compatibility. - The SDK is used in diverse environments with different module requirements. - Developer experience and maximum flexibility are priorities. For most modern projects, the `dual` format is best. It ensures the SDK works smoothly in any environment, while still providing the performance benefits of ESM when used with modern bundlers. ## Additional reading - [TypeScript configuration options](/docs/gen-reference/ts-config) - [Lean SDKs with standalone functions](/post/standalone-functions) # Disabling barrel files Source: https://speakeasy.com/docs/sdks/customize/typescript/disabling-barrel-files import { Callout } from "@/mdx/components"; By default, the SDK generator creates **barrel files** (`index.ts` files) that centralize module re-exports within an SDK package. ## Configuring barrel file generation The `useIndexModules` configuration option controls barrel file generation. Configure this option by adding or modifying `useIndexModules` under the `typescript` section in the `gen.yaml` file: ```yaml filename="/.speakeasy/gen.yaml" typescript: # add or modify `useIndexModules` useIndexModules: true # or false # other TypeScript configuration options... ``` By default, `useIndexModules` is set to `true` so that Speakeasy generates barrel files that look like this: ```typescript filename="/src/models/errors/index.ts" // petstore sdk export * from "./apierror.js"; export * from "./apierrorinvalidinput.js"; export * from "./apierrornotfound.js"; export * from "./apierrorunauthorized.js"; export * from "./httpclienterrors.js"; export * from "./sdkvalidationerror.js"; ``` SDK consumers can then import modules from barrel files as follows: ```typescript filename="example.ts" // somewhere in a consumer's code import { ApiErrorInvalidInput, ApiErrorNotFound, ApiErrorUnauthorized, SDKValidationError, } from "petstore/models/errors"; ``` If you set `useIndexModules` to `false`, Speakeasy will not generate these barrel files. Consumers need to import modules directly from the individual module files: ```typescript filename="example.ts" // somewhere in a consumer's code import { ApiErrorInvalidInput } from "petstore/models/errors/apierrorinvalidinput.js"; import { ApiErrorNotFound } from "petstore/models/errors/apierrornotfound.js"; import { ApiErrorUnauthorized } from "petstore/models/errors/apierrorunauthorized.js"; import { SDKValidationError } from "petstore/models/errors/sdkvalidationerror.js"; ``` ## Pros and cons of using barrel files Barrel files provide a convenient way to centralize imports for SDK consumers. When `useIndexModules` is `true`, users can use a single entry point to import modules, rather than having to supply the exact file structure of the package's resources. On the other hand, disabling barrel files has some advantages: - **Superior tree shaking performance**: Barrel files can significantly impair effective tree shaking in modern bundlers. When importing modules from a barrel file, bundlers often struggle to determine which exports are actually used, leading to the inclusion of unused code. Disabling barrel files allows bundlers to accurately track dependencies and exclude unused code, resulting in smaller production bundles. - **Enhanced developer tooling**: Without barrel files, IDE "Go to Definition" features (for example, `Cmd`/`Ctrl` + Click) link directly to the source file containing the implementation, rather than through an intermediate barrel file. This improves code navigation and makes it easier to understand the codebase structure. - **Faster build performance**: Barrel files create additional import chains that build tools have to process. When you remove these intermediary files: - Build tools process fewer files during compilation - Module dependency graphs become simpler and more efficient - Hot module replacement (HMR) becomes more precise - TypeScript type checking performance improves Disabling barrel files provides optimal tree shaking performance when combined with `moduleFormat: dual`. The ESM format's static analysis capabilities work best with direct imports, allowing bundlers to eliminate unused code more effectively. ## Making the right choice Whether you should use `useIndexModules` depends on your project's priorities: - **Disable barrel files** (`useIndexModules: false`) when: - Bundle size optimization is critical - You're using modern bundlers like Webpack, Rollup, or Vite - Build performance is important - Your team values clear dependency paths - **Keep barrel files** (`useIndexModules: true`) when: - Import convenience is more important than bundle optimization - Your project doesn't use a bundler with tree shaking - You prefer centralized import management For modern web applications, we recommend setting `useIndexModules: false`. While barrel files offer convenient imports, the performance benefits of direct imports typically outweigh this convenience, especially when using modern IDEs with good import management features. This article draws information from [Why you should avoid barrel files in JavaScript](https://laniewski.me/blog/pitfalls-of-barrel-files-in-javascript-modules/) by Bartosz Łaniewski and [Please Stop Using Barrel Files](https://tkdodo.eu/blog/please-stop-using-barrel-files) by Dominik Dorfmeister. ## Additional reading Visit the following pages to learn more about generating SDKs with Speakeasy: - [TypeScript configuration options](/docs/customize/typescript/configuring-module-format) - [Lean SDKs with standalone functions](/post/standalone-functions) # Model validation and serialization Source: https://speakeasy.com/docs/sdks/customize/typescript/model-validation-and-serialization Speakeasy TypeScript SDKs support model validation and serialization backed by Zod. This feature enables validation of data against models and easy serialization or deserialization of data to and from JSON. ## Example As an example, consider the following model definition: ```typescript filename="/src/models/components/book.ts" import * as z from "zod"; export type Book = { /** * The unique identifier for the book */ id: string; /** * The title of the book */ title: string; /** * The author of the book */ author: string; }; ``` ### From JSON ```typescript filename="example.ts" import { bookFromJSON, bookToJSON } from "my-sdk/models/components/book"; const result = bookFromJSON('{"id":"1","title":"1984","author":"George Orwell"}'); if (result.ok) { console.log(result.value); // 👆 result.value is of type `Book` } else { // Handle validation errors console.error(result.error); } ``` ### To JSON ```typescript filename="example.ts" const jsonString = bookToJSON({ id: "1", title: "1984", author: "George Orwell" }); // jsonString is of type `string` ``` ## Forward compatibility and fault tolerance Speakeasy TypeScript SDKs are designed to handle API evolution gracefully. The deserialization layer includes several features that make SDKs more resilient to changes in API responses. ### Forward-compatible enums Enums in responses automatically accept unknown values, so new enum values added by the API won't break existing SDK users. Unknown values are captured in a type-safe `Unrecognized` wrapper. Configure this behavior with `forwardCompatibleEnumsByDefault` in your `gen.yaml`, or control individual enums with `x-speakeasy-unknown-values: allow` or `x-speakeasy-unknown-values: disallow` in your OpenAPI spec. ### Forward-compatible unions Discriminated unions accept unknown discriminator values, capturing them with their raw data accessible via the `UNKNOWN` discriminator value. Configure this with `forwardCompatibleUnionsByDefault: tagged-only` in your `gen.yaml`. ### Lax mode When `laxMode: lax` is enabled (the default), the SDK gracefully handles missing or mistyped fields by filling in sensible zero-value defaults rather than failing deserialization. This ensures SDKs continue working even when API responses don't perfectly match the expected schema, while maintaining type safety. ### Smart union deserialization The `unionStrategy: populated-fields` setting (enabled by default) picks the union option with the most matching fields, making deserialization more robust when union types aren't well-discriminated. For more details, see the [forward compatibility documentation](/docs/sdks/manage/forward-compatibility) and the [TypeScript configuration reference](/docs/speakeasy-reference/generation/ts-config). # Control Property Naming and Casing Source: https://speakeasy.com/docs/sdks/customize/typescript/property-naming import { Callout } from "@/mdx/components"; TypeScript SDKs support configurable property naming to match your API's field naming conventions. You can either normalize property names to a consistent casing style, or preserve the exact names from your OpenAPI spec. ## Property Casing Use `modelPropertyCasing` to control whether generated TypeScript types use camelCase or snake_case property names. ### Configuration ```yaml filename="/.speakeasy/gen.yaml" typescript: modelPropertyCasing: "camel" # or "snake" ``` ### Options - **`camel` (default)**: Properties use camelCase naming (e.g., `firstName`, `createdAt`) - **`snake`**: Properties use snake_case naming (e.g., `first_name`, `created_at`) ### Examples **camelCase naming (default)** ```typescript // Configuration: modelPropertyCasing: "camel" export type User = { id: string; firstName: string; lastName: string; emailAddress: string; createdAt: Date; isActive: boolean; }; ``` **snake_case naming** ```typescript // Configuration: modelPropertyCasing: "snake" export type User = { id: string; first_name: string; last_name: string; email_address: string; created_at: Date; is_active: boolean; }; ``` ## Preserve Original Property Names Use `preserveModelFieldNames` to keep the exact property names from your OpenAPI spec without any normalization. This is useful when: - Your API uses unconventional naming (e.g., properties starting with `_`) - You need exact parity with existing SDKs or API documentation - Your property names have special meaning that would be lost through normalization ### Configuration ```yaml filename="/.speakeasy/gen.yaml" typescript: preserveModelFieldNames: true ``` When `preserveModelFieldNames` is enabled, `modelPropertyCasing` only affects synthetic fields generated by Speakeasy (like `additionalProperties`, `clientID`, etc.). Properties defined in your OpenAPI spec will use their original names exactly as specified. ### Example Given an OpenAPI spec with these properties: ```yaml components: schemas: Event: type: object properties: _raw: type: string __metadata: type: object event_type: type: string EventID: type: string ``` **Without preservation (default):** ```typescript // preserveModelFieldNames: false (default) // Properties are normalized based on modelPropertyCasing export type Event = { raw: string; // Leading underscores removed metadata: object; // Leading underscores removed eventType: string; // Converted to camelCase eventId: string; // Converted to camelCase }; ``` **With preservation:** ```typescript // preserveModelFieldNames: true // Properties match OpenAPI spec exactly export type Event = { _raw: string; // Preserved with underscore __metadata: object; // Preserved with underscores event_type: string; // Preserved as snake_case EventID: string; // Preserved as PascalCase }; ``` ### When to Use | Scenario | Recommendation | |----------|----------------| | Standard APIs with conventional naming | Use `modelPropertyCasing` (default) | | APIs with leading underscores (e.g., `_raw`, `_links`) | Use `preserveModelFieldNames: true` | | Migrating from existing SDK with specific field names | Use `preserveModelFieldNames: true` | | Mixed naming conventions that must be preserved | Use `preserveModelFieldNames: true` | # Generating React Hooks from OpenAPI Source: https://speakeasy.com/docs/sdks/customize/typescript/react-hooks import { Callout, CodeWithTabs } from "@/mdx/components"; Speakeasy can optionally generate React Hooks as part of your TypeScript SDK. React Hooks are built from your OpenAPI document using TanStack Query (formerly React Query), making it easier to build web applications that interact with your API. Generated React Query hooks are available as part of the Speakeasy [business tier](/pricing). ## How to generate React Hooks from an OpenAPI document Follow these steps to generate React Hooks from your OpenAPI document: ### Install the Speakeasy CLI If you haven't already, install the Speakeasy CLI: ### Generate a TypeScript SDK If you haven't already generated a TypeScript SDK, run this command: ```bash # For first-time SDK generation speakeasy quickstart ``` Then, follow the prompts to select TypeScript as your target language and complete the SDK configuration. ### Enable React Hooks Open the `.speakeasy/gen.yaml` file in your SDK directory and add the `enableReactQuery` flag to it: ```yaml typescript: version: x.y.z # ... other options enableReactQuery: true # Add this line to enable React hooks ``` ### Regenerate your SDK with React Hooks Navigate to the folder that holds the generated SDK and run the following command: ```bash speakeasy run ``` This adds a new `src/react-query` directory, which contains all the generated React Hooks, to your SDK. ### Install the SDK in your application Once you've generated your SDK with React Hooks, you need to install it in your React application. - If you've published your SDK to a package registry like npm, install it directly: - Alternatively, if you're working with a local SDK, use the package manager's local path installation feature: ## How to use the generated React Hooks Before using React Hooks, you need to install their dependencies and configure your React Query provider. ### Install required dependencies The generated SDK with React Hooks has TanStack Query as a dependency that should be listed in the SDK's `package.json`. Zod is bundled with the SDK. However, you might need to install TanStack Query explicitly in your application: The Speakeasy-generated TypeScript SDK bundles Zod for runtime type validation, and TanStack Query powers the React hooks. TanStack Query is required for the React hooks to function correctly. ### Set up the React Query provider Add the TanStack Query provider to your React application: ```tsx import React from "react"; import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; import ReactDOM from "react-dom/client"; import App from "./App"; // Create a client const queryClient = new QueryClient(); ReactDOM.createRoot(document.getElementById("root")!).render( , ); ``` ## Basic usage You can use the generated Hooks in your components as follows: ```tsx import { useProductsGetById } from "your-sdk/react-query"; function ProductDetail({ productId }) { const { data, isLoading, error } = useProductsGetById(productId); if (isLoading) return
Loading...
; if (error) return
Error: {error.message}
; return (

{data.name}

{data.description}

${data.price}

); } ``` ## How to customize React Hooks The default naming convention for React Hooks follows the standalone function naming convention. For example, a standalone function called `productsGetById` will have a corresponding React Hook called `useProductsGetById`. Sometimes, these names are not ideal for React Hooks. In those instances, you can use the `x-speakeasy-react-hook` OpenAPI extension to override the default name: ```yaml filename="openapi.yaml" paths: /products/{id}: get: operationId: getById tags: [products] x-speakeasy-react-hook: name: Product # ... ``` With the example above, the React Hook will be called by `useProduct` and, under the hood, it will use the `productsGetById` standalone function. ### Generate queries and mutations By default, the `GET`, `HEAD`, and `QUERY` operations generate React Query hooks, while other operations generate mutation hooks. This isn't always correct, since certain operations may be "read" operations exposed under the `POST` endpoint. A great example of this is the complex search APIs that take a request body with filters and other arguments. You can override these OpenAPI operations with the `x-speakeasy-react-hook` extension so that they come out as React Query hooks: ```yaml filename="openapi.yaml" paths: /search/products: post: operationId: productsSearch x-speakeasy-react-hook: name: Search type: query # ... ``` ### Disable React Hooks for an operation React Hooks may not be useful or relevant for all operations in the API. Disable React Hook generation for an operation by setting the `disabled` flag under the `x-speakeasy-react-hook` extension: ```yaml filename="openapi.yaml" paths: /admin/reports: post: operationId: generateReport x-speakeasy-react-hook: disabled: true # ... ``` ## Advanced usage You can also use React Hooks for more advanced purposes. ### Implement optimistic updates When using mutation hooks, you can use TanStack Query's optimistic update capabilities: ```tsx import { useQueryClient } from "@tanstack/react-query"; import { useProductsGetById, useProductsUpdate, } from "your-sdk-package/dist/react-query"; function EditProduct({ productId }) { const queryClient = useQueryClient(); const { data: product } = useProductsGetById(productId); const { mutate, isLoading } = useProductsUpdate({ onSuccess: (data) => { // Invalidate and refetch queryClient.invalidateQueries(["products", productId]); }, }); const handleSubmit = (updatedData) => { // Optimistically update to the new value queryClient.setQueryData(["products", productId], updatedData); // Then update on the server mutate({ id: productId, data: updatedData, }); }; // Component implementation } ``` ### Customize configurations You can pass custom TanStack Query options to your hooks: ```tsx const { data } = useProductsGetById(productId, { // Query options refetchInterval: 5000, // Refetch every 5 seconds staleTime: 1000 * 60 * 5, // Consider data fresh for 5 minutes retry: 3, // Retry failed requests 3 times }); ``` ### Use infinite queries for pagination For paginated endpoints, you can use infinite React Query hooks: ```tsx import { useProductsListInfinite } from "your-sdk-package/dist/react-query"; function ProductsList() { const { data, fetchNextPage, hasNextPage, isFetching } = useProductsListInfinite({ limit: 10 }); return (
{data?.pages.map((page, i) => ( {page.products.map((product) => ( ))} ))}
); } ``` ## Next steps Visit the following resources to: - Learn about [customizing your TypeScript SDK](/docs/customize/typescript/additional-index-exports) - Explore how to set up your [SDK documentation](/docs/sdk-docs/edit-readme) - Check out the [TanStack Query documentation](https://tanstack.com/query/latest/docs/react/overview) for advanced use cases # Add webhooks to your SDKs Source: https://speakeasy.com/docs/sdks/customize/webhooks import { Callout, Screenshot, Table } from "@/mdx/components"; ## Why use the webhooks feature? - **Built-in SDK support:** It simplifies webhook adoption through built-in SDK support. - **Abstracted complexity:** Consumers don't need to worry about cryptographic operations or dependencies. - **Default security:** Consumers must verify the signature to unpack the webhook data. - **Type-safe consumption:** Consumers get schema-validated data and types. - **Type-safe sending:** Producers can send schema-validated data through type-safe SDK methods. ## Getting started Webhooks are a paid add-on, [reach out to us to discuss pricing](https://www.speakeasy.com/book-demo). 1. You must have a [Speakeasy Business or Enterprise plan](/pricing). 2. Enable the webhooks add-on to your account under `settings/billing`: ## Example You can see the full source code for this example in the [webhooks example repo](https://github.com/speakeasy-api/examples/tree/main/webhooks-ts). ### OpenAPI ```yaml openapi: 3.1.0 # You must use OpenAPI 3.1.0 or higher info: title: Petstore API version: 1.0.0 servers: - url: https://petstore.swagger.io/v2 paths: /pets: post: operationId: addPet requestBody: required: true content: application/json: schema: $ref: "#/components/schemas/Pet" responses: "200": description: Okay x-speakeasy-webhooks: security: type: signature headerName: x-signature signatureTextEncoding: base64 algorithm: hmac-sha256 webhooks: pet.created: post: operationId: sendPetCreated requestBody: required: true content: application/json: schema: $ref: "#/components/schemas/PetCreated" responses: "200": description: Okay pet.deleted: post: operationId: sendPetDeleted requestBody: required: true content: application/json: schema: $ref: "#/components/schemas/PetDeleted" responses: "200": description: Okay components: schemas: PetCreated: type: object properties: type: type: string enum: - pet.created # This is the payload discriminator pet: $ref: "#/components/schemas/Pet" required: - type - pet PetDeleted: type: object properties: type: type: string enum: - pet.deleted # This is the payload discriminator id: type: string required: - type - id Pet: type: object properties: id: type: string required: - id ``` ### For webhook consumers The `validateWebhook()` function is currently only implemented in the TypeScript SDK, with support for additional languages planned for future releases. While other languages will generate webhook types, this discriminator method is TypeScript-only. ```typescript import { Petstore } from "petstore"; const sdk = new Petstore(); const data = await sdk.validateWebhook({ request, secret: "", }); console.log(data); if (data.type === "pet.created") { console.log("Pet created", data.pet); } if (data.type === "pet.deleted") { console.log("Pet deleted", data.id); } ``` **Error handling** ```typescript import { Petstore } from "petstore"; import { SDKValidationError } from "petstore/models/errors/sdkvalidationerror.js"; import { WebhookAuthenticationError } from "petstore/types/webhooks.js"; const sdk = new Petstore(); try { await sdk.validateWebhook({ request, secret: "", }); } catch (error) { if (error instanceof WebhookAuthenticationError) { // Thrown when signature verification fails, usually due to: // - Incorrect webhook secret // - Modified request payload // - Wrong signature format console.error("Webhook authentication failed", error); } if (error instanceof SDKValidationError) { // Thrown when the webhook request body is unrecognised, usually due // to an outdated SDK version or un-docummented payloads console.error("Webhook request body is invalid", error); } throw error; } ``` ### For webhook producers ```typescript import { Petstore } from "petstore"; const sdk = new Petstore(); const data = await sdk.sendPetCreated( { url: "https://example.com/my-webhook-handler", secret: "", }, { type: "pet.created", pet: { id: "dog" }, }, ); ``` ## `x-speakeasy-webhooks` The `x-speakeasy-webhooks` extension is used to define the webhooks for your API.
### `x-speakeasy-webhooks.security`
### `x-speakeasy-webhooks.security.type: signature` ```yaml x-speakeasy-webhooks: security: type: signature headerName: x-signature signatureTextEncoding: base64 algorithm: hmac-sha256 ``` This will apply HMAC SHA256 to the body of the webhook request and put it in a header. ### `x-speakeasy-webhooks.security.type: custom` ```yaml x-speakeasy-webhooks: security: type: custom ``` This generates the `src/hooks/webhooks-custom-security.ts` boilerplate file, which you can then use to modify the security logic. See the source code for this example in the [GitHub repo](https://github.com/speakeasy-api/examples/tree/main/webhooks-custom-security-ts). # Using the CLI to populate code samples without GitHub Actions Source: https://speakeasy.com/docs/sdks/guides/code-samples-without-github-actions This guide explains how to use the Speakeasy CLI to generate and publish code samples for your API documentation when you're not using GitHub Actions. ## Overview Speakeasy automatically generates code samples for your OpenAPI document and makes them available via a public URL that can be integrated with documentation platforms like Scalar. While this process is typically automated with GitHub Actions, you can achieve the same result using just the Speakeasy CLI in your own custom workflow. ## Prerequisites To follow this guide, you need: - **The Speakeasy CLI:** Install the CLI on your computer. - **An OpenAPI document:** Speakeasy generates your SDKs based on your OpenAPI document - **A Speakeasy account and API key:** Ensure you have access to the Speakeasy workspace. - **A CI environment:** The generation must run from a CI environment or with the `CI_ENABLED` environment variable set to `true`. ## Generating code samples with the Speakeasy CLI Follow these steps to populate your OpenAPI document and make it available for use with documentation platforms: ### Configure your workflow First, set up your workflow configuration file: ```bash speakeasy configure ``` This creates a `.speakeasy/workflow.yaml` file that defines your SDK generation targets and code samples configuration. ### Generate SDKs and code samples Run the Speakeasy CLI to generate your SDKs and code samples: ```bash speakeasy run ``` This command: - Downloads or loads your OpenAPI document - Validates the OpenAPI document - Generates SDKs for your configured languages - Creates code samples for each operation in your API ### Promote code samples to main The critical step for enabling automated code samples is to tag both your source specification (OpenAPI document) and generated code samples with the `main` tag: ```bash speakeasy tag promote -s my-source-name -c my-target-name -t main ``` This command tags both the source OpenAPI document (`-s my-source-name`) and the generated code samples (`-c my-target-name`) as "official" so they can be incorporated into the public URL. Similar to how the `main` branch in GitHub represents the production-ready version of your code, the `main` tag in Speakeasy indicates these are the production-ready specifications and code samples that should be publicly available. Replace `my-source-name` and `my-target-name` with the names defined in your workflow configuration. ### Access the public URL Once you've tagged your code samples with `main`, Speakeasy automatically starts building a combined OpenAPI document in the background. The combined document is available at a public URL that you can use with documentation platforms like Scalar. To find this URL, open the Speakeasy workspace and navigate to the **Docs** tab. Click **Integrate with Docs Provider** to see the URL to the combined OpenAPI document with populated code samples. Currently, there's no CLI command to retrieve this URL programmatically. ### Integrate with docs providers Once you have the public URL, you can integrate it with various documentation providers. Speakeasy offers detailed integration guides for several popular docs platforms: - [Scalar](/docs/sdk-docs/integrations/scalar) is a modern API documentation platform. - [ReadMe](/docs/sdk-docs/integrations/readme) offers an interactive API explorer and documentation. - [Mintlify](/docs/sdk-docs/integrations/mintlify) provides developer documentation with an interactive playground. - [Bump.sh](/docs/sdk-docs/integrations/bump) hosts API documentation and catalogs. ## Automating the process Use the following steps to create a fully automated workflow without GitHub Actions: - Create a script or CI pipeline that runs `speakeasy run` to generate SDKs and code samples. - Add `speakeasy tag promote -s my-source-name -c my-target-name -t main` to tag both the source OpenAPI document and generated code samples. - The public URL automatically updates the OpenAPI document with the latest code samples. ### Why use CLI tagging instead of GitHub Actions? While GitHub Actions provides a convenient way to automate code sample generation and tagging, the CLI approach offers several advantages for teams: - **Platform independence:** Use any CI/CD system (such as Jenkins, GitLab CI, CircleCI, or Azure DevOps) instead of being limited to GitHub Actions. - **Custom workflows:** Integrate code sample generation into existing build processes or deployment pipelines. - **Local development:** Generate and test code samples locally before pushing changes. - **Private repositories:** Work with code that isn't hosted on GitHub or is hosted in private repositories with restricted access. - **Enterprise environments:** Provide support for organizations with specific security or compliance requirements that prevent them from using GitHub Actions. ### Example workflow script Here's a simple bash script example that could be used in a custom CI pipeline: ```bash #!/bin/bash # Install Speakeasy CLI if needed # curl -fsSL https://raw.githubusercontent.com/speakeasy-api/speakeasy/main/install.sh | sh # Set CI environment variable if not running in CI export CI_ENABLED=true # Generate SDKs and code samples speakeasy run # Tag both source specification and code samples as main speakeasy tag promote -s my-source-name -c my-python-target -t main speakeasy tag promote -s my-source-name -c my-typescript-target -t main echo "Code samples have been generated and tagged. They will be available at the public URL in the Speakeasy dashboard." ``` # What is a Monorepo? Source: https://speakeasy.com/docs/sdks/guides/creating-a-monorepo import { Callout } from "@/mdx/components"; This section outlines an advanced setup process that may not be suitable for all users. For most use cases, we recommend adopting the simpler approach of a single SDK per GitHub repository. To setup a single SDK per Github see our [SDK quickstart](/docs/sdks/create-client-sdks). A monorepo is a unified repository containing multiple SDKs, each corresponding to a unique OpenAPI specification. This approach offers a centralized location for discovering available SDKs while allowing for the independent download and management of each SDK as needed. Each SDK resides in its own subfolder, and can be made complete with individual GitHub workflows for regeneration and release processes. ## Repository Structure The monorepo's structure is designed for scalability and easy navigation. In our example, we will discuss two SDKs: ServiceA and ServiceB, each found in their respective subfolders. The typical structure of such a monorepo is as follows: ```yaml .github/workflows/ - serviceA_generate.yaml # Github Workflow for generating the ServiceA SDK, generated by speakeasy cli - serviceA_release.yaml # Github Workflow for releasing and publishing the ServiceA SDK, generated by speakeasy cli - serviceB_generate.yaml # Github Workflow for generating the ServiceB SDK, generated by speakeasy cli - serviceB_release.yaml # Github Workflow for releasing and publishing the ServiceB SDK, generated by speakeasy cli .speakeasy/workflow.yaml # Speakeasy workflow file that dictates mapping of sources (eg: openapi docs) and targets (eg: language sdks) to generate serviceA/ - gen.yaml # Generation config for the ServiceA SDK serviceB/ - gen.yaml # Generation config for the ServiceB SDK ``` This structure can be expanded to accommodate any number of SDKs. ## Creating Your SDK Monorepo You have two options for creating your SDK monorepo with Speakeasy and GitHub: starting from a template or building from scratch. For the purpose of this guide, we will use an example with two APIs, one for `lending` and one for `accounting`. ### Option 1: Use a Template Start by cloning the [`template-sdk-monorepo` repository](https://github.com/speakeasy-sdks/template-sdk-monorepo?tab=readme-ov-file) using the "Use template" button on GitHub, and name it as you see fit. ### Option 2: Build from Scratch 1. Create a new repository on GitHub and clone it down locally with `git clone `. 2. Mimic the directory structure shown above. This can be achieved easily by following the interactive CLI commands below a. Install Speakeasy CLI b. Use quickstart to boostrap the repository. This will create the necessary directories and files for you to start generating SDKs. ```bash speakeasy quickstart ``` When prompted make sure to choose a sub directory rather than the root of your directory for generating the first target. We will add more targets in the following steps. c. Configure your sources ```bash speakeasy configure sources ``` For each source reference a local or remote OpenAPI document. Optionally add in an overlay if needed. Each source you configure here will be used to point towards a generation target in the following step. ![Screenshot of configuring sources for monorepo.](/assets/guides/monorepo-step-2c.png) d. Configure your targets For each target referenced a local or remote OpenAPI document. Optionally add in an overlay if needed. For ever target make sure to choose a a language, a source and a subdirectory to generate the target in. In the provided example `accounting` and `lending` are two sub directories in which we'll generate two different targets for two different sources. ```bash speakeasy configure targets ``` ![Screenshot of configuring targets for monorepo.](/assets/guides/monorepo-step-2d.png) #### Final Speakeasy Workflow The final speakeasy workflow file will look like the following ```yaml workflowVersion: 1.0.0 speakeasyVersion: latest sources: accounting: inputs: - location: /path/to/accounting/openapi.yaml lending: inputs: - location: /path/to/lending/openapi.yaml targets: accounting-ts: target: typescript source: accounting output: ./accounting lending-ts: target: typescript source: lending output: ./lending ``` #### Setup Github Release and Publishing Workflows Finally if you want your SDKs to be regenerated and published in their Github repos setup the workflow files needed for remote generation and publishing. The CLI can help you set up these files with: ```bash speakesy configure publishing ``` Follow the prompts to setup your secrets in Github Action Secrets. Push up the repo to your remote location and watch everything spin! ### Configure Generation Edit the `gen.yaml` file in the root of each SDK's subfolder. This file dictates the generator settings, including package name, class names, parameter inlining, and other preferences. For complete documentation on all the available publishing configurations, see [here](/docs/speakeasy-reference/generation/gen-yaml). ## Generate SDKs ### Locally Simply run `speakeasy run` and select the SDKs you wish to regenerate. Use `speakaesy run --force` if there are no OpenAPI document changes. ![Screenshot of running all targets for monorepo.](/assets/guides/monorepo-step-run.png) ### Github To manually trigger SDK generation: 1. Navigate to the Actions tab in your GitHub repository. 2. Select the generation workflow for the SDK you wish to generate. 3. Click "Run workflow" to start the generation process. Check mark the "Force" input option if there are no OpenAPI document changes. ## Verify your SDK After generation, review the SDK to ensure it aligns with your requirements. # Generate SDK in a Subdirectory Source: https://speakeasy.com/docs/sdks/guides/generate-in-a-subdirectory import { Screenshot } from "@/mdx/components"; Similar to setting up a monorepo, you can also configure Speakeasy to generate your SDK into a subdirectory. This setup maybe useful in maintaining seperation between handwritten and generated code or for consuming the SDK in a monolithic codebase. ## Step 1: In the root of your existing directory run `speakeasy quickstart`: ## Step 2: Select your generation language. ## Step 3: Name your package. ## Step 4: If you're setting your SDK up in a folder with existing subfolders, you will be prompted to select an output directory. ## Step 5: Complete generating your SDK. Going forward to generate your SDK navigate to your SDK folder and run `speakeasy run`. # Authenticating with local environment variables Source: https://speakeasy.com/docs/sdks/guides/hooks/env-auth-hook When authenticating with an API using a SDK, its a common pattern for the value of an `API_KEY` or `token` to default to the value of an environment variable. This allows you to easily switch between different environments without changing the code. In this example, we'll show you how to use a [SDK Hook](/docs/customize/code/sdk-hooks) enable your users to authenticate with your API using local environment variables. A SDK Hook is a function that will be executed by the SDK at a specific point in the request lifecycle. For this use case we'll leverage a `BeforeRequest` hook. Inside of our Speakeasy generated SDK hooks are written in the `src/hooks/` directory. We'll make a new hook called in a file called `auth.ts`. ```typescript filename="src/hooks/auth.ts" import { BeforeRequestHook } from "./types"; export const injectAPIKey: BeforeRequestHook = { beforeRequest: async (_, request) => { const authz = request.headers.get("Authorization"); if (authz) { return request; } let token = ""; if (typeof process !== "undefined") { token = process.env["API_KEY"] ?? ""; } if (!token) { throw new Error("The API_KEY environment variable is missing or empty; either provide it"); } request.headers.set("Authorization", `Bearer ${token}`); return request; }, }; ``` This hook will check for the presence of an environment variable named `API_KEY` and if it exists, it will add it to the `Authorization` header of the request. Finally to ensure the SDK uses this hook, we need to add it to make sure it is registered with the SDK. This is done in the `src/hooks/registration.ts` file. ```typescript filename="src/hooks/registration.ts" import { injectAPIKey } from "./auth"; import { Hooks } from "./types"; /* * This file is only ever generated once on the first generation and then is free to be modified. * Any hooks you wish to add should be registered in the initHooks function. Feel free to define them * in this file or in separate files in the hooks folder. */ export function initHooks(hooks: Hooks) { // Add hooks by calling hooks.register{ClientInit/BeforeRequest/AfterSuccess/AfterError}Hook // with an instance of a hook that implements that specific Hook interface // Hooks are registered per SDK instance, and are valid for the lifetime of the SDK instance hooks.registerBeforeRequestHook(injectAPIKey); } ``` Finally make sure to update the usage snippet in your readme to reference the environment variable. # SDK Hooks Source: https://speakeasy.com/docs/sdks/guides/hooks import { CardGrid } from "@/mdx/components"; import { hookGuidesData } from "@/lib/data/docs/hook-guides"; # Add telemetry to your SDK with SDK hooks and Posthog Source: https://speakeasy.com/docs/sdks/guides/hooks/posthog-telemetry-hook ## Prerequisites - You will need a Posthog account (If you don't have one, you can sign up [here](https://posthog.com/signup)) ## Overview This guide will walk you through adding telemetry to a TypeScript SDK using SDK hooks and the Posthog Node SDK. SDK hooks are a way to inject custom actions at various points in the SDK's execution. You can inject custom actions at the following points in the SDK's execution: - `On SDK Initialization` - `Before a request is executed` - `After a successful response` - `After an error response` ## Adding the Posthog SDK to your project To add the Posthog SDK to your project, you will need to add the dependency to your Speakeasy SDK's `gen.yaml` file under the dependancies section: ```yaml configVersion: 2.0.0 generation: sdkClassName: Petstore ... typescript: version: 0.7.11 additionalDependencies: dependencies: posthog-node: ^4.0.1 <- This is the line you need to add, ensure the version you add adheres to NPM package standards. ``` After adding the dependency, the Posthog SDK will be included in your projects package.json file every time you generate your SDK. ## Adding your first SDK hook Now that you have the Posthog SDK included in your project, you can start adding SDK hooks to your SDK. First, create a new file in the `src/hooks` directory, and name it `telemetry_hooks.ts`. In this file you will need to import the hook types for each hook you want to use, as well as the PostHog SDK and initialize the PostHog SDK with your API Key. I will be using all four of the hooks in this guide, but you can choose which hooks you want to use. ```typescript import { PostHog } from "posthog-node"; import { AfterErrorContext, AfterErrorHook, AfterSuccessContext, AfterSuccessHook, BeforeRequestContext, BeforeRequestHook, SDKInitHook, SDKInitOptions, } from "./types"; const PostHogClient = new PostHog("phc_xxxxxxxxxxxxxxxxxx", { host: "https://us.i.posthog.com", }); ``` Next you can create a class that will hold your hooks. Our class will be `TelemetryHooks` and our first hook will be an `On SDK Initialization` hook. Below is the start of our `TelemetryHooks` class. This class will hold all of our telemetry hooks. ```typescript export class TelemetryHooks implements SDKInitHook { sdkInit(opts: SDKInitOptions): SDKInitOptions { const { baseURL, client } = opts; return { baseURL, client }; } } ``` This hook allows us to inject custom actions and capture an `SDK Init` event to Posthog at the time the SDK is initialized. Here are the key points to the capture method outlined below: - **distinctId**: A `distinctId` can be provided, serving as a unique identifier for either a user or a session. This is particularly useful for tracking recurring events across different sessions, which can aid in identifying and troubleshooting issues. - **event**: The name of the event is specified to facilitate easier sorting and analysis within Posthog. - **properties**: An arbitrary set of properties; extra information relevant to the event. Here the contents of the `opts` parameter are added to the event as properties. This allows for detailed tracking of the initialization parameters. Lastly Posthog's SDKs are asynchronous, so we need to shutdown the SDK after we are done, ensuring events are flushed out before the process ends. ```typescript export class TelemetryHooks implements SDKInitHook { sdkInit(opts: SDKInitOptions): SDKInitOptions { const { baseURL, client } = opts; PostHogClient.capture({ distinctId: "distinct_id_of_the_user", event: "SDK Init", properties: { baseURL, client, }, }); PostHogClient.shutdown(); return { baseURL, client }; } } ``` Now that we have our `TelemetryHooks` class, we can add the remainder of the hooks. ```typescript export class TelemetryHooks implements SDKInitHook, BeforeRequestHook, AfterSuccessHook, AfterErrorHook {// <- add in the remainder of the hooks you will be implementing ... } ``` The structure of the remaining hooks is the same, We just supply a `distinctId`, an `event`, and `properties` for each hook. ```typescript async beforeRequest( hookCtx: BeforeRequestContext, request: Request ): Promise { PostHogClient.capture({ distinctId: "distinct_id_of_the_user", event: "Before Request", properties: { hookCtx: hookCtx, }, }); await PostHogClient.shutdown(); return request; } async afterSuccess( hookCtx: AfterSuccessContext, response: Response ): Promise { PostHogClient.capture({ distinctId: "distinct_id_of_the_user", event: "After Success", properties: { hookCtx: hookCtx, response: response, }, }); await PostHogClient.shutdown(); return response; } async afterError( hookCtx: AfterErrorContext, response: Response | null, error: unknown ): Promise<{ response: Response | null; error: unknown }> { PostHogClient.capture({ distinctId: "distinct_id_of_the_user", event: "After Error", properties: { hookCtx: hookCtx, response: response, error: error, }, }); await PostHogClient.shutdown(); return { response, error }; } ``` Once all the hooks are implemented, you can now use the `TelemetryHooks` class in your SDK. in the `src/hooks/registration.ts`, you simply need to import the class from the file you created the hooks in, and register them following the directions in the comment. ```typescript import { TelemetryHooks } from "./telemetry_hooks"; import { Hooks } from "./types"; /* * This file is only ever generated once on the first generation and then is free to be modified. * Any hooks you wish to add should be registered in the initHooks function. Feel free to define them * in this file or in separate files in the hooks folder. */ export function initHooks(hooks: Hooks) { // Add hooks by calling hooks.register{ClientInit/BeforeCreateRequest/BeforeRequest/AfterSuccess/AfterError}Hook // with an instance of a hook that implements that specific Hook interface // Hooks are registered per SDK instance, and are valid for the lifetime of the SDK instance hooks.registerBeforeRequestHook(new TelemetryHooks()); hooks.registerAfterSuccessHook(new TelemetryHooks()); hooks.registerAfterErrorHook(new TelemetryHooks()); hooks.registerSDKInitHook(new TelemetryHooks()); } ``` You can now regenerate your SDK and use the new hooks. Running API calls using this SDK will surface events up to Posthog. And you can review all the details in Posthog. ![Screenshot of event data in Posthog.](/assets/guides/posthog-event-data.png) You can review all the code outlined in this guide in the [SDK Hooks](https://github.com/speakeasy-api/examples/blob/main/posthog-hook-ts/src/hooks/posthog.ts) repository. # Capture errors with SDK hooks and Sentry Source: https://speakeasy.com/docs/sdks/guides/hooks/sentry-error-hook ## Prerequisites You will need: - A Sentry account [(If you don't have one, you can sign up [here](https://sentry.io/signup))] - A Sentry project ## Overview This guide will show you how to use SDK hooks to integrate error collection into a TypeScript SDK with the Sentry Node SDK, allowing you to insert custom actions at various stages of the SDK's execution: - `On SDK Initialization` - `Before a request is executed` - `After a successful response` - `After an error response` ## Adding the Sentry SDK to your project To add the Sentry SDK to your project, you will need to add the dependency to your Speakeasy SDK's `gen.yaml` file under the dependancies section: ```yaml configVersion: 2.0.0 generation: sdkClassName: Petstore ... typescript: version: 0.8.4 additionalDependencies: dependencies: '@sentry/node': ^8.9.2 # <- This is the line you need to add, ensure the version you add adheres to NPM package standards. ``` After adding the dependency, the Sentry SDK will be included in your projects package.json file every time you generate your SDK. ## Adding your first SDK hook With the Sentry SDK included in your project, you can start adding SDK hooks. 1. Create a new file: In the `src/hooks` directory, create a file named `error_hooks.ts`. 2. Import hook types and Sentry SDK: In this file, import the necessary hook types and initialize the Sentry SDK with your project's DSN. ```typescript import * as Sentry from "@sentry/node"; import { AfterErrorContext, AfterErrorHook } from "./types"; Sentry.init({ dsn: process.env.SENTRY_DSN, // <- This is your Sentry DSN, you can find this in your Sentry project settings. }); ``` Next create an `ErrorHooks` class to hold your hooks. ```typescript export class ErrorHooks implements AfterErrorHook { afterError( hookCtx: AfterErrorContext, response: Response | null, error: unknown, ): { response: Response | null; error: unknown } { return { response, error }; } } ``` This hook allows us to inject custom code that runs on error responses and capture an `SDK Error` event to Sentry at the time the error occurs. Here are the key points to the class outlined below: - Use the `AfterErrorHook` interface to define the type of the hook. - Capture the `hookCtx`, `response`, and `error` in the `afterError` method. - Return the `response` and `error` so that the SDK can continue to process the error. Specific notes for using Sentry here: - Add a breadcrumb to Sentry to capture additional details regarding the error. - capturing the error in Sentry using `Sentry.captureException(error)`. ```typescript export class ErrorHooks implements AfterErrorHook { afterError( hookCtx: AfterErrorContext, response: Response | null, error: unknown, ): { response: Response | null; error: unknown } { Sentry.addBreadcrumb({ category: "sdk error", message: "An error occurred in the SDK", level: "error", data: { hookCtx, response, error, }, }); Sentry.captureException(error); return { response, error }; } } ``` Once this hook is implemented, you can now use the `ErrorHooks` class in your SDK. In the `src/hooks/registration.ts` file import and register the class from the file you created the hooks in: ```typescript import { ErrorHooks } from "./error_hooks"; import { Hooks } from "./types"; /* * This file is only ever generated once on the first generation and then is free to be modified. * Any hooks you wish to add should be registered in the initHooks function. Feel free to define them * in this file or in separate files in the hooks folder. */ export function initHooks(hooks: Hooks) { // Add hooks by calling hooks.register{ClientInit/BeforeCreateRequest/BeforeRequest/AfterSuccess/AfterError}Hook // with an instance of a hook that implements that specific Hook interface // Hooks are registered per SDK instance, and are valid for the lifetime of the SDK instance hooks.registerAfterErrorHook(new ErrorHooks()); } ``` You can now regenerate your SDK and use the new hooks. Running API calls with this SDK will send error events to your Sentry project, where you can review all the details for each error. ![Screenshot of error data in Sentry.](/assets/guides/sentry-error-data.png) Review all the code outlined in this guide by visiting the [SDK Hooks](https://github.com/speakeasy-api/examples/blob/main/sentry-hook-ts/src/hooks/sentry.ts) repository. # Setting user agents in browser environments Source: https://speakeasy.com/docs/sdks/guides/hooks/user-agent-hook ## Overview When using Speakeasy SDKs in browser environments, setting the user agent header directly is restricted by browser security policies. This guide demonstrates how to use SDK hooks to set user agent information in a browser-compatible way. ## Understanding the challenge Browsers prevent direct modification of the `User-Agent` header for security reasons. When attempting to set this header in browser environments: - The header modification may silently fail - No error will be thrown - The original browser user agent will be used instead ## Solution using SDK hooks We can use a `BeforeRequestHook` to implement a fallback mechanism that ensures our SDK version information is properly transmitted, even in browser environments. ```typescript import { BeforeRequestContext, BeforeRequestHook, Awaitable } from "./types"; import { SDK_METADATA } from "../lib/config"; export class CustomUserAgentHook implements BeforeRequestHook { beforeRequest(_: BeforeRequestContext, request: Request): Awaitable { const version = SDK_METADATA.sdkVersion; const ua = `speakeasy-sdk/${version}`; // Try to set the standard user-agent header first request.headers.set("user-agent", ua); // Check if the header was actually set (it may silently fail in browsers) if (!request.headers.get("user-agent")) { // Fall back to a custom header if the user-agent couldn't be set request.headers.set("x-sdk-user-agent", ua); } return request; } } ``` ## How the hook works 1. The hook attempts to set the standard `user-agent` header first 2. It then checks if the header was successfully set 3. If the header wasn't set (which happens in browsers), it falls back to using a custom header `x-sdk-user-agent` 4. This ensures your SDK version information is always transmitted, regardless of browser restrictions ## Adding the hook to your SDK Register the hook in your SDK's hook registration file: ```typescript import { CustomUserAgentHook } from "./user_agent"; import { Hooks } from "./types"; export function initHooks(hooks: Hooks) { hooks.registerBeforeRequestHook(new CustomUserAgentHook()); } ``` ## Using hooks with generated SDKs When working with SDKs generated by Speakeasy, you'll want to follow these best practices: - Always implement the fallback mechanism to handle browser environments - Use a consistent format for your user agent string (e.g., `sdk-name/version`) - Consider including additional relevant information in the user agent string - Test the implementation in both browser and non-browser environments This approach ensures your Speakeasy SDK can properly identify itself to your API while remaining compatible with browser security restrictions. # SDK Guides Source: https://speakeasy.com/docs/sdks/guides import { CardGrid } from "@/mdx/components"; import { sdkGuidesData } from "@/lib/data/docs/sdk-guides"; # SDK Overlays Source: https://speakeasy.com/docs/sdks/guides/overlays import { CardGrid } from "@/mdx/components"; import { overlayGuidesData } from "@/lib/data/docs/overlay-guides"; # Creating Internal and External SDKs Source: https://speakeasy.com/docs/sdks/guides/overlays/internal-external-versions To create two different versions of an SDK, one for internal use and one for external use, use JSONPath expressions in [OpenAPI Overlays](/openapi/overlays) (a standard extension for modifying existing OpenAPI documents without changing the original). This approach dynamically targets and hides internal operations and parameters from the public SDK. The [`workflow.yaml` file](/docs/workflow-file-reference) can be configured to include Overlays as part of the source definition. ### Using the `x-internal` Extension First, add an `x-internal: true` extension to all the operations, parameters, and other elements in the OpenAPI spec that should only be available in the internal SDK. ```yaml filename="api.yaml" info: title: Sample API version: 1.0.0 description: A sample API with internal paths and parameters. paths: /public-resource: get: summary: Retrieve public data responses: '200': description: Public data response content: application/json: schema: type: object properties: id: type: string name: type: string /internal-resource: x-internal: true # This path is internal and should not be exposed externally get: summary: Retrieve internal data responses: '200': description: Internal data response content: application/json: schema: type: object properties: id: type: string internalInfo: type: string description: Internal information x-internal: true # This field is internal description: "This endpoint is restricted for internal staff management and not visible in public SDKs." ``` ### Using JSONPath Expressions in an Overlay Next, use a JSONPath expression to remove all the internal paths and parameters from the SDK. This removal occurs specifically when generating the internal SDK. ```yaml filename="internal-overlay.yaml" info: title: Sample API Overlay version: 1.0.0 actions: - target: $.paths.*.*[?(@["x-internal"] == true)] remove: true - target: $.paths.*.*[?(@.properties[?(@["x-internal"] == true)])] remove: true ``` Define a workflow that generates both the internal and external SDKs. ```yaml filename="workflow.yaml" workflowVersion: 1.0.0 speakeasyVersion: latest sources: external-api: inputs: - location: ./api.yaml overlays: - location: ./external-overlay.yaml internal-api: inputs: - location: ./api.yaml overlays: - location: ./internal-overlay.yaml targets: internal-sdk: target: python source: internal-api external-sdk: target: python source: external-api ``` # What are JSONPath Expressions? Source: https://speakeasy.com/docs/sdks/guides/overlays/json-path-expressions JSONPath expressions provide a powerful tool for querying and manipulating JSON data in your OpenAPI specifications. By using JSONPath, you can selectively target specific nodes in your API spec and apply modifications without altering the base structure ## Example JSONPath Expressions * [All Post or Get Operations](#all-post-or-get-operations) * [Operations with a Request Body](#operations-with-a-request-body) * [Operations with a Specific Visibility Notation](#operations-with-a-specific-visibility-notation) * [Inject Annotations as Siblings](#inject-annotations-as-siblings) ### All Post or Get Operations This expression selects all paths that contain either POST or GET operations. It's useful when you want to apply changes or add annotations specifically to these HTTP methods. **JSONPath Target** ``` $.paths.*[?("post","get")] ``` **Overlay Action** ```yaml actions: - target: $.paths.*[?("post","get")] update: x-apiture-logging: true description: "Logging enabled for all POST and GET operations." ``` ### Operations with a Request Body This targets all operations that include a request body. It's ideal for adding descriptions, examples, or additional schema validations to request bodies. **JSONPath Target** ``` $.paths.*[?(@.requestBody)] ``` **Overlay Action** ```yaml actions: - target: $.paths.*[?(@.requestBody)] update: x-custom-validation: "custom-validation-schema" description: "Custom validations applied to request bodies." ``` ### Operations with a Specific Visibility Notation This expression is used to find all operations marked with 'admin' visibility. It can be used to apply additional security measures or modify documentation for admin-only endpoints. **JSONPath Target** ``` $.paths.*[?(@["x-visibility"] == "admin")] ``` **Overlay Action** ```yaml actions: - target: $.paths.*[?(@["x-visibility"] == "admin")] update: x-custom-security: "enhanced" description: "Enhanced security for admin endpoints." ``` ### Inject Annotations as Siblings This expression targets all operations within an OpenAPI specification that have an `operationId` property. **JSONPath Target** ``` $.paths.*.*[?(@.operationId)] ``` **Overlay Action** ```yaml actions: - target: $.paths.*.*[?(@.operationId)] update: x-detailed-logging: "enabled" log_level: "verbose" description: "Verbose logging enabled for this operation to track detailed performance metrics." ``` # Example Overlays Source: https://speakeasy.com/docs/sdks/guides/overlays/overlays [Overlays](/docs/prep-openapi/overlays/create-overlays) act as a layer on top of your existing OpenAPI specification, allowing you to apply modifications and extensions seamlessly. They are perfect for adding new features, customizing responses, and adapting specifications to specific needs without disrupting the underlying structure. To demonstrate the power of overlays, we'll use a sample OpenAPI Specification for "The Speakeasy Bar." This initial spec sets the stage for our overlay examples: ```yaml filename="openapi.yaml" openapi: 3.1.0 info: title: The Speakeasy Bar version: 1.0.0 summary: A bar that serves drinks. servers: - url: https://speakeasy.bar description: The production server. security: - apiKey: [] tags: - name: drinks description: The drinks endpoints. - name: orders description: The orders endpoints. paths: /dinner/: post: ... get: ... /drinks/: post: ... ``` Let's explore how overlays can enhance and adapt this specification to do the following: * [Adding Speakeasy Extensions](#adding-speakeasy-extensions) * [Adding SDK Specific Documentation](#adding-sdk-specific-documentation) * [Modifying AutoGenerated Schemas](#modifying-autogenerated-schemas) * [Adding Examples to API Documentation](#adding-examples-to-api-documentation) * [Hiding Internal APIs from a Public SDK](#hiding-internal-apis-from-a-public-sdk) * [Removing specific PUT operation](#removing-specific-put-operation) * [Standardize Configurations](#standardize-configurations) ### Adding Speakeasy Extensions **Objective:** Integrate [Terraform](/docs/terraform/customize/entity-mapping) functionality into the API specification for order management. **Overlay Action**: ```yaml filename="overlay.yaml" overlay: 1.0.0 info: title: Add Terraform Functionality to Order Schema version: 1.1.0 actions: - target: "$.components.schemas.Order" update: - x-speakeasy-entity: Order description: "Enables Terraform provider support for the Order schema." ``` ### Adding SDK Specific Documentation **Objective:** Provide tailored instructions for Java and JavaScript SDKs for the `/orders` endpoint. **Overlay Action**: ```yaml filename="overlay.yaml" overlay: 1.0.0 info: title: Distinguish Order Endpoint Docs for Java and JavaScript SDKs version: 1.1.1 actions: - target: "$.paths['/orders'].post.description" update: - value: "For Java SDK: use `OrderService.placeOrder()`. For JavaScript SDK: use `orderService.placeOrder()`." ``` ### Modifying Autogenerated Schemas **Objective:** Enhance the precision of the Drink schema, making it more descriptive and informative for API consumers. ```yaml filename="overlay.yaml" overlay: 1.0.0 info: title: Refine Drink Schema for Better Clarity version: 1.1.2 actions: - target: "$.components.schemas.Drink" update: - properties: type: type: string description: "Type of drink, e.g., 'cocktail', 'beer'." alcoholContent: type: number description: "Percentage of alcohol by volume." ``` ### Adding Examples to API Documentation **Objective:** Illustrate the drink ordering process with a practical example for user clarity. ```yaml filename="overlay.yaml" overlay: 1.0.0 info: title: Add Drink Order Example for User Clarity version: 1.1.3 actions: - target: "$.paths['/drinks/order'].post" update: - examples: standardOrder: summary: "Standard order example" value: drink: "Old Fashioned" quantity: 1 ``` ### Hiding Internal APIs from a Public SDK **Objective:** Restrict the visibility of internal staff management endpoints. ```yaml filename="overlay.yaml" overlay: 1.0.0 info: title: Secure Internal Staff Management Endpoint version: 1.1.4 actions: - target: "$.paths['/internal/staff']" update: - x-internal: true description: "This endpoint is restricted for internal staff management and not visible in public SDKs." ``` ### Removing Specific Put Operation **Objective:** Exclude PUT operations without the `x-speakeasy-entity-operation.` **Overlay Action**: ```yaml filename="overlay.yaml" overlay: 1.0.0 info: title: Remove Non-Essential PUT Operations version: 1.1.0 actions: - target: $.paths.*.put[?(! @.x-speakeasy-entity-operation)] remove: true ``` ### Standardize Configurations **Objective:** Remove the server and security configurations from each operation within the paths. **Overlay Action**: ```yaml filename="overlay.yaml" overlay: 1.0.0 info: title: Standardize Server and Security Configurations version: 1.1.0 actions: - target: $.paths.*.*.servers remove: true - target: $.paths.*.*.security remove: true ``` # override-compile Source: https://speakeasy.com/docs/sdks/guides/override-compile ## Overriding Compile Commands for SDK Generation ### 1. Remove `package.json` from `.genignore` The `.genignore` file is used to signal which files are manually managed rather than handled by the SDK generator. It functions similarly to `.gitignore` but for SDK generation purposes. Update your `.genignore` file to remove `package.json`. It no longer needs to be ignored as the generation process will manage it automatically. ### 2. Create a Compile Script Create a file named `openapi/scripts/compile.sh` and add the following script: ```bash #!/usr/bin/env bash set -e npm install npm run build ``` Ensure the script is executable by running the following command: ```bash chmod +x openapi/scripts/compile.sh ``` ### 3. Update `gen.yaml` Modify your `.speakeasy/gen.yaml` file to include the `compileCommand` under the TypeScript section. Add the following configuration: ```yaml typescript: compileCommand: - bash - -c - ./openapi/scripts/compile.sh ``` ### 4. Verify the Configuration Run the following command to test that the setup is working as expected: ```bash speakeasy run --force ``` # Switching default package manager to `pnpm` Source: https://speakeasy.com/docs/sdks/guides/pnpm-default import { Callout } from "@/mdx/components"; ## Prerequisite - A GitHub repository with the Speakeasy [SDK Generation GitHub Action](https://github.com/speakeasy-api/sdk-generation-action) integrated and enabled. ## Adding `pnpm` Support 1. Open the GitHub Actions workflow file (e.g., `.github/workflows/sdk-generation.yaml`). 2. Modify the `generate` job to include the `pnpm_version` input. This ensures pnpm is installed during the action. ### Example Workflow File ```yaml name: Generate permissions: checks: write contents: write pull-requests: write statuses: write "on": workflow_dispatch: inputs: force: description: Force generation of SDKs type: boolean default: false set_version: description: Optionally set a specific SDK version type: string jobs: generate: uses: speakeasy-api/sdk-generation-action/.github/workflows/workflow-executor.yaml@v15 with: force: ${{ github.event.inputs.force }} mode: pr set_version: ${{ github.event.inputs.set_version }} speakeasy_version: latest working_directory: packages/sdk pnpm_version: "9.19.4" # Specify the required pnpm version secrets: github_access_token: ${{ secrets.GITHUB_TOKEN }} npm_token: ${{ secrets.NPM_TOKEN_ELEVATED }} speakeasy_api_key: ${{ secrets.SPEAKEASY_API_KEY }} ``` ## (Optional) Verifying `pnpm` Installation Ensure pnpm is used in the workflow by adding a step to verify its presence: ```yaml steps: - name: Verify pnpm installation run: pnpm --version ``` This outputs the installed `pnpm` version for confirmation during workflow execution. ## Additional Notes - Use the same `pnpm_version` as used in local development for consistency. - Ensure any `package.json` files are compatible with pnpm. Run `pnpm install` locally to verify. ## Using PNPM in Monorepos When working with monorepos, pnpm offers several advantages including strict module resolution and efficient workspace management. To configure Speakeasy to use pnpm in your monorepo: ### Local Development Configuration Add the `compileCommand` configuration to your `gen.yaml` file: ```yaml typescript: compileCommand: - pnpm - install ``` ### Workspace Configuration Ensure your `pnpm-workspace.yaml` includes the SDK directory: ```yaml packages: - "packages/*" - "sdk/*" # Include your SDK location ``` ### Benefits for Monorepos - **Strict module resolution**: Prevents dependency confusion between packages - **Efficient storage**: Shared dependencies across workspace packages - **Better workspace support**: Built-in monorepo tooling For comprehensive monorepo setup and troubleshooting tips, see our [TypeScript Monorepo Tips guide](/guides/sdks/typescript-monorepo-tips). # Manage OpenAPI specs with Speakeasy Source: https://speakeasy.com/docs/sdks/guides/publish-specs-to-api-registry import { Callout } from "@/mdx/components"; For the quickstart to set up SDKs, see the [SDK Quickstart](/docs/sdks/create-client-sdks). This guide walks through the process of managing OpenAPI specs using the Speakeasy API Registry and CLI. This is also known as a "source-only workflow" as it only uses the sources feature of [Speakeasy workflows](/docs/speakeasy-reference/workflow-file). ## Prerequisites - A Speakeasy account and CLI installed. Log in [here](https://app.speakeasy.com/) to create an account. - An OpenAPI spec for the API ## Overview To get started, create a GitHub repository. Something like `company-specs` or `company-openapi` are common repository names. Initialize a new workflow file by running `speakeasy configure sources` and follow the prompts to add in the OpenAPI spec and any [overlays](/docs/prep-openapi/overlays/create-overlays). Make sure to give the `source` a meaningful name as this will be the name used for the API Registry in the workspace dashboard. A common repository structure will look like the following: ```yaml company-specs ├── .speakeasy/ │ ├── workflow.yaml ├── specs │ ├── openapi.yaml │ ├── ... ├── overlays │ ├── overlay.yaml │ ├── ... ├── overlayed_specs # This folder includes specs that have been modified by overlays │ ├── openapi_modified.yaml │ ├── ... └── .gitignore ``` The workflow file will be generated in the `.speakeasy` folder and will look like the following: ```yaml workflowVersion: 1.0.0 speakeasyVersion: latest sources: my-api: # This is the name of the source used in `speakeasy configure sources` inputs: - location: specs/openapi.yaml registry: location: registry.speakeasy.com/your-company/your-company/my-api outputs: - location: overlayed_specs/openapi_modified.yaml targets: {} ``` ## Publishing spec changes Any time a new version of the OpenAPI spec should be published to the API Registry, run: ```bash speakeasy run -s target ``` This will publish the spec to the API Registry as a new revision for the source `my-api`. Optionally tag the revision of a spec by running: ```bash speakeasy tag apply -n v0.0.2 ``` This will tag the most recent revision of the spec with the tag `v0.0.2`. ## API Registry This will show up as the latest revision in the API Registry tab in the dashboard. ![Registry Tab](/assets/guides/registry-tab.png) The API Registry can be used to: - Track changes to specs, view detailed change reports, and download past versions of the spec. - Create a stable public URL for sharing a particular revision of the spec. - Use the registry URI as a source for generating SDKs, for example: `registry.speakeasy.com/your-company/your-company/my-api`. ## Triggering downstream SDK generation For teams that manage SDKs in separate repositories, the spec repo can trigger SDK generation in downstream repos when changes are made. This pattern is useful when: - A central team manages the API specification - Multiple SDK repositories need to be generated from the same spec - SDK generation should be triggered automatically when the spec changes - SDK PRs should be created in downstream repos for review before merging ### Architecture overview ``` ┌─────────────────────────────────────────────────────────────────────────┐ │ Spec Repository │ │ ┌─────────────┐ ┌─────────────────────────────────────────────┐ │ │ │ OpenAPI │ │ GitHub Actions Workflow │ │ │ │ Spec │───▶│ 1. Runs `speakeasy run` to tag spec │ │ │ │ (PR change) │ │ 2. Triggers downstream SDK workflows │ │ │ └─────────────┘ │ 3. Updates PR comment with status │ │ │ └─────────────────────────────────────────────┘ │ └─────────────────────────────────────────────────────────────────────────┘ │ │ gh workflow run ▼ ┌─────────────────────────────────────────────────────────────────────────┐ │ Downstream SDK Repos │ │ ┌──────────────────────────┐ ┌──────────────────────────┐ │ │ │ TypeScript SDK │ │ Python SDK │ │ │ │ - Pulls tagged spec │ │ - Pulls tagged spec │ │ │ │ - Generates SDK │ │ - Generates SDK │ │ │ │ - Creates PR │ │ - Creates PR │ │ │ └──────────────────────────┘ └──────────────────────────┘ │ └─────────────────────────────────────────────────────────────────────────┘ ``` ### Setting up downstream SDK repositories Each downstream SDK repository needs a workflow that accepts `workflow_dispatch` inputs. After running `speakeasy configure github`, modify the generated workflow to accept these inputs: ```yaml on: workflow_dispatch: inputs: force: description: "Force SDK regeneration" required: false default: "false" feature_branch: description: "Branch for SDK changes" required: false environment: description: "Environment variables (e.g., TAG=branch-name)" required: false ``` Pass these inputs to the workflow executor: ```yaml jobs: generate: uses: speakeasy-api/sdk-generation-action/.github/workflows/workflow-executor.yaml@v15 with: mode: pr force: ${{ inputs.force }} feature_branch: ${{ inputs.feature_branch }} environment: ${{ inputs.environment }} secrets: github_access_token: ${{ secrets.GITHUB_TOKEN }} speakeasy_api_key: ${{ secrets.SPEAKEASY_API_KEY }} ``` The SDK repo's `.speakeasy/workflow.yaml` should reference the registry with a `$TAG` variable: ```yaml sources: my-api: inputs: - location: registry.speakeasyapi.dev/your-company/your-company/my-api@$TAG ``` ### Required secrets On the spec repository, configure these secrets: | Secret | Description | |--------|-------------| | `SPEAKEASY_API_KEY` | Authenticates the Speakeasy CLI. Obtain from the [Speakeasy Platform](https://app.speakeasy.com) API Keys page. | | `DOWNSTREAM_SDK_TOKEN` | A fine-grained GitHub PAT with `actions: write`, `contents: write`, and `pull-requests: write` permissions on all downstream SDK repos. | On each downstream SDK repository: | Secret | Description | |--------|-------------| | `SPEAKEASY_API_KEY` | Authenticates the Speakeasy CLI. Can be the same key as the spec repo or a separate key. | ### Example implementation A complete working example is available in the [examples-downstream-spec-repo](https://github.com/speakeasy-api/examples-downstream-spec-repo) repository, which triggers SDK generation in: - [examples-downstream-spec-sdk-typescript](https://github.com/speakeasy-api/examples-downstream-spec-sdk-typescript) - [examples-downstream-spec-sdk-python](https://github.com/speakeasy-api/examples-downstream-spec-sdk-python) The example includes workflows that: - Trigger SDK generation when a PR is opened on the spec repo - Update the spec PR with comments showing generation status and links to SDK PRs - Automatically merge or close SDK PRs when the spec PR is merged or closed # Tips for Integrating a TypeScript SDK into a Monorepo Source: https://speakeasy.com/docs/sdks/guides/typescript-monorepo-tips import { Callout } from "@/mdx/components"; If you're looking to set up a monorepo from scratch, check out our [Create a monorepo](/guides/sdks/creating-a-monorepo) guide first. ## Dependency Confusion The most common issue we see is dependency confusion. This happens when developers have multiple versions of the same dependency across different packages in the monorepo. ### Why is this a problem? Let's say a developer has Zod installed in their monorepo's root, and their Speakeasy SDK also uses Zod. With Speakeasy's bundled Zod dependency, this is no longer an issue since the SDK uses its own version of Zod v3, preventing conflicts with user installations of Zod v4. Previously, developers might have run into a situation where code like this didn't work: ```typescript try { const result = await sdk.products.create({ name: "Cool Product", price: "not a number", // This should fail validation }); } catch (err) { if (err instanceof ZodError) { // This check could fail with peer dependencies! console.log("Validation error:", err.errors); } } ``` The `instanceof` check could fail because they were importing `ZodError` from a different instance of Zod than what the SDK was using. By bundling Zod, Speakeasy eliminates this confusion. ### How to fix it PNPM's strict module resolution is fantastic at preventing this issue: ```bash # In .npmrc shamefully-hoist=false strict-peer-dependencies=true ``` If you're using Yarn or npm, you'll need to be more explicit: ```json // With Yarn (in package.json) { "resolutions": { "zod": "^3.22.4", "@tanstack/react-query": "^4.29.5" } } // With npm (in package.json) { "overrides": { "zod": "^3.22.4", "@tanstack/react-query": "^4.29.5" } } ``` This forces all packages to use the same version of these dependencies, preventing the confusion. ## Module format mismatches - ESM vs CommonJS chaos The second most common issue is dealing with mixed module formats. Some packages use CommonJS, others use ESM, and a monorepo probably has a mix of both. ### Why this is a problem You might see errors like: ``` Error [ERR_REQUIRE_ESM]: require() of ES Module not supported ``` Or: ``` SyntaxError: Cannot use import statement outside a module ``` These happen when module systems don't align. It's especially common when there's a package using CommonJS trying to import an ESM module, or vice versa. ### How to fix it The simplest solution is to configure your Speakeasy SDK to use ESM format: ```yaml # In gen.yaml typescript: moduleFormat: esm ``` If you need to support both ESM and CommonJS packages importing your SDK, you can use the dual format (which is the Speakeasy default): ```yaml # In gen.yaml typescript: moduleFormat: dual ``` When using `moduleFormat: dual`, make sure the tsconfig.json is set up correctly: ```json // In tsconfig.json { "compilerOptions": { "moduleResolution": "node", "module": "esnext", "esModuleInterop": true } } ``` There's a small bundle size tradeoff with dual format, but it's usually worth it for the compatibility benefits. ## Package manager differences - npm vs the world The last common issue is package manager compatibility. Speakeasy uses npm when building SDKs, but a monorepo might use pnpm, Yarn, or even Bun. ### Why this is a problem Different package managers handle dependencies differently. This can lead to subtle issues where the SDK works fine when generated, but breaks when integrated into the monorepo. For example, one might see errors about missing dependencies that they know are installed, or weird resolution issues that only happen in the monorepo. ### How to fix it Configure Speakeasy to use your preferred package manager when building the SDK. For pnpm (recommended for monorepos), customize the compile command in your `gen.yaml`: ```yaml # For pnpm typescript: compileCommand: - pnpm - install ``` This tells Speakeasy to use pnpm instead of npm when building the SDK, which is especially important in monorepos where pnpm's strict module resolution helps prevent dependency confusion issues. For more detailed information about configuring pnpm as your default package manager, see our [Using PNPM guide](/guides/sdks/pnpm-default). ## Putting it all together - a real-world example This is an example of how a developer may set up a monorepo with a Speakeasy SDK: ``` my-monorepo/ ├── package.json ├── pnpm-workspace.yaml ├── tsconfig.json ├── packages/ │ ├── api-sdk/ # Speakeasy-generated SDK │ │ ├── package.json │ │ ├── tsconfig.json │ │ └── gen.yaml │ ├── frontend/ # Frontend application using the SDK │ │ ├── package.json │ │ └── tsconfig.json │ └── backend/ # Backend service using the SDK │ ├── package.json │ └── tsconfig.json ``` pnpm-workspace.yaml: ```yaml packages: - "packages/*" ``` frontend/package.json: ```json { "dependencies": { "api-sdk": "workspace:*" } } ``` gen.yaml: ```yaml typescript: moduleFormat: esm compileCommand: - pnpm - install - and - pnpm - build ``` This setup gives: 1. A consistent dependency tree with pnpm's strict module resolution 2. ESM modules for maximum compatibility 3. pnpm for package management ## Best practices for TypeScript SDKs in monorepos Beyond the specific issues above, here are some general best practices: - **Use workspace references**: Always reference your SDK using workspace syntax (`workspace:*`) rather than local file paths - **Consistent TypeScript versions**: Use the same TypeScript version across all packages - **Shared tsconfig**: Create a base tsconfig.json that all packages extend - **Centralized types**: Consider creating a shared types package for common interfaces - **Integration tests**: Write tests that verify the SDK works correctly with other packages ## Related documentation For more information on configuring Speakeasy TypeScript SDKs, check out: - [TypeScript SDK Design](https://www.speakeasy.com/docs/languages/typescript/methodology-ts) - [Configuring Module Format](https://www.speakeasy.com/docs/customize/typescript/configuring-module-format) - [TypeScript SDK Reference](https://www.speakeasy.com/docs/languages/typescript/feature-support) - [Model Validation and Serialization](https://www.speakeasy.com/docs/customize/typescript/model-validation-and-serialization) - [TypeScript Configuration](https://www.speakeasy.com/docs/speakeasy-reference/generation/ts-config) # Utilizing User Agent Strings for Telemetry Source: https://speakeasy.com/docs/sdks/guides/utilizing-user-agent-strings ## Overview Each Speakeasy SDK incorporates a unique user agent string in all HTTP requests made to the API. This string helps identify the source SDK and provides details like the SDK version, the generator version, the document version, and the package name. ## Format The format of the user agent string across all Speakeasy SDKs is as follows: ``` speakeasy-sdk/<> {{SDKVersion}} {{GenVersion}} {{DocVersion}} {{PackageName}} ``` Components: * `Language`: The language of the SDK (e.g., C#, Java, Python). * `SDKVersion`: The version of the SDK, specified in `gen.yaml`. * `GenVersion`: The version of the Speakeasy generator used. * `DocVersion`: The version of the OpenAPI document that generated the SDK. * `PackageName`: The name of the package as defined in `gen.yaml`. For a Java SDK, the user agent string might look like this: ```speakeasy-sdk/java 2.3.1 1.5.0 2022.01 1.0.0 speakeasyJavaClient``` ## Parsing User Agent Strings To accurately parse user agent strings, you can use regular expressions, string manipulation techniques, or dedicated parsing libraries. Below is a Python example using regex and Flask: ```python from flask import request import re @app.route('/some-path') def some_function(): user_agent = request.headers.get('User-Agent') # Regex pattern to match the user agent string pattern = r"^speakeasy-sdk/(?P\w+)\s+(?P[\d\.]+)\s+(?P[\d\.]+)\s+(?P\S+)\s+(?P[\w\.\-]+)$" # Match the user agent string against the regex pattern match = re.match(pattern, user_agent) if match: details = match.groupdict() print(details) ``` ## Utilizing Parsed Data for Telemetry Parsed user agent strings can enhance telemetry in several ways: **Data Enrichment**: With parsed user agent strings, you can enhance telemetry records by appending key metadata such as the SDK version, programming language, and package details. This enriched data supports more detailed segmentation and sophisticated analysis, improving the granularity and usability of your telemetry insights. This approach allows you to track feature adoption across different segments and tailor your development focus accordingly. **Monitoring SDK Usage**: Utilize the detailed data from user agent strings to monitor the adoption rates of different SDK versions and the prevalence of programming languages among your users. This intelligence is crucial for informed decision-making regarding SDK updates and deprecation schedules. By understanding which versions are most popular and how quickly users adopt new releases, you can better manage support resources and communication strategies. **Performance Analysis**: Correlate specific SDK versions or configurations with performance metrics like response times and error rates. This analysis helps pinpoint whether recent updates have improved performance or introduced new issues, allowing your development team to target fixes and optimizations more effectively. Regularly reviewing these correlations helps maintain high performance standards and ensures a positive user experience. **Anomaly Detection**: Implement advanced anomaly detection techniques in your telemetry to automatically flag unusual activity, such as a sudden decrease in the usage of a normally popular SDK version or an unexpected increase in error rates following a new release. Early detection of these anomalies can prompt swift action, potentially averting more significant issues and enhancing customer satisfaction. This proactive monitoring is essential in maintaining the reliability and credibility of your SDKs. # Introduction to Speakeasy SDKs Source: https://speakeasy.com/docs/sdks/introduction import { productCards } from "@/lib/data/docs/introduction"; import { CardGrid, CodeWithTabs } from "@/mdx/components"; Speakeasy helps teams build great developer experiences for their APIs. We provide the tools, workflows, and infrastructure to generate and manage high-quality SDKs, Terraform providers, and API documentation—directly from your OpenAPI spec. With Speakeasy, you can go from an OpenAPI definition to a fully versioned, type-safe SDK in minutes, complete with publishing, CI/CD, and changelog automation. ## Why APIs matter APIs are a powerful force for innovation. One team solves a problem, exposes an API, and every engineer (or AI agent) benefits from their work. That means more time spent tackling new problems, and less time reinventing the wheel. The problem is that most APIs are bad. The tools and practices for building quality, reliable APIs haven't kept pace with the central role APIs play in modern software development. That's the problem Speakeasy exists to solve. ## Generate with Speakeasy ## Before you Begin ### Sign up Sign up for a free Speakeasy account at [https://app.speakeasy.com](https://app.speakeasy.com). {/* */} New accounts start with a 14-day free trial of Speakeasy's business tier, enabling users to try out every SDK generation feature. At the end of the trial, accounts will revert to the free tier. No credit card is required. Free accounts can continue to generate one SDK with up to 50 API methods free of charge. ### Install the Speakeasy CLI Install the Speakeasy CLI using one of the following methods: ## Workflow
![ref_architecture](/assets/docs/ref-architecture.png)
The platform is built to be OpenAPI-native, no proprietary DSLs to cause lock-in. From OpenAPI specs, the platform enables generation of SDKs, API documentation, agent tools & more. To make it seamless, we provide native CI/CD workflows that automate updates, from backend changes through to SDK release management. ## Support We operate as an extension of our customers' API platform teams. We have dedicated support to help with sensitive releases and provide feedback on API design & best practices. # methodology-cpp Source: https://speakeasy.com/docs/sdks/languages/cpp/methodology-cpp Coming soon! Stay tuned for updates. # Generate C# SDKs from OpenAPI / Swagger Source: https://speakeasy.com/docs/sdks/languages/csharp/methodology-csharp import { FileTree } from "nextra/components"; ## SDK Overview The Speakeasy C# SDK is designed to support the .NET ecosystem, including publishing to [NuGet](https://www.nuget.org/). The .NET version to build against is configurable to either `.NET 8.0` (default), `.NET 6.0`, or `.NET 5.0`. The SDK is designed to be strongly typed, light on external dependencies, easy to debug, and easy to use. Some of its core features include: - Interfaces for core components allow for dependency injection and mocking. - Generated C# doc comments to enhance IntelliSense compatibility and developer experience. - Async/await support for all API calls. - Optional pagination support for supported APIs. - Support for complex number types: - `System.Numbers.BigInteger` - `System.Decimal` - Support for string- and integer-based enums. The SDK includes minimal dependencies. The only external dependencies are: - `newtonsoft.json` for JSON serialization and deserialization. - `nodatime` for date and time handling. ## C# Package Structure ## HTTP Client By default, the C# SDK will instantiate its own `SpeakeasyHttpClient`, which uses the `System.Net.HttpClient` under the hood. The default client can be overridden by passing a custom HTTP client when initializing the SDK: ```csharp var sdk = new SDK(client: new CustomHttpClient()); ``` The provided HTTP client must implement the `ISpeakeasyHttpClient` interface as defined in `Utils.SpeakeasyHttpClient.cs`: ```csharp using .Utils; public class CustomHttpClient : ISpeakeasyHttpClient { public CustomHttpClient() { // initialize client } /// /// Sends an HTTP request asynchronously. /// /// The HTTP request message to send. /// The value of the TResult parameter contains the HTTP response message. public virtual async Task SendAsync(HttpRequestMessage request) { // implement the send method } /// /// Clones an HTTP request asynchronously. /// /// /// This method is used in the context of Retries. It creates a new HttpRequestMessage instance /// as a deep copy of the original request, including its method, URI, content, headers, and options. /// /// The HTTP request message to clone. /// The value of the TResult parameter contains the cloned HTTP request message. public virtual async Task CloneAsync(HttpRequestMessage request) { // implement the clone method } } ``` This is useful if you're using a custom HTTP client that supports proxy settings or other custom configuration. In the example below, a custom client inherits from the internal `SpeakeasyHttpClient` class, which implements the `ISpekeasyHttpClient` interface. This client adds a header to all requests before sending them: ```csharp using .Utils; internal class CustomSpeakeasyHttpClient : SpeakeasyHttpClient { public CustomSpeakeasyHttpClient() {} public override async Task SendAsync(HttpRequestMessage request) { request.Headers.Add("X-Custom-Header", "custom value"); return await base.SendAsync(request); } } ``` ## Data Types and Classes The C# SDK uses as many native types from the standard library as possible, for example: - `string` - `System.DateTime` - `int` - `long` - `System.Numberics.BigInteger` - `float` - `double` - `decimal` - `bool` The C# SDK will only fall back on custom types when the native types are not suitable, for example: - `NodaTime.LocalDate` for `date` types - Custom `enum` types for `string` and `integer` based enums Speakeasy will generate standard C# classes with public fields that use attributes to guide the serialization and deserialization processes. ## Parameters If parameters are defined in the OpenAPI document, Speakeasy will generate methods with parameters. The number of parameters defined should not exceed the `maxMethodParams` value configured in the `gen.yaml` file. If they do or the `maxMethodParams` value is set to `0`, Speakeasy will generate a request object that allows for all parameters to be passed in a single object. ## Errors The C# SDK will throw exceptions for any network or invalid request errors. For unsuccessful responses, if a custom error response is specified in the spec file, the SDK will unmarshal the HTTP response details into the custom error response and throw it as an exception. If no custom response is specified in the spec, the SDK will throw an `SDKException` containing details of the failed response. ```csharp using Openapi; using Openapi.Models.Shared; using System; using Openapi.Models.Errors; using Openapi.Models.Operations; var sdk = new SDK(); try { var res = await sdk.Errors.StatusGetXSpeakeasyErrorsAsync(statusCode: 385913); // handle success } catch (Exception ex) { if (ex is StatusGetXSpeakeasyErrorsResponseBody) { // handle custom exception response } else if (ex is Openapi.Models.Errors.SDKException) { // handle standard exception response } } ``` ## User Agent Strings The C# SDK will include a [user agent](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/User-Agent) string in all requests that can be leveraged for tracking SDK usage amongst broader API usage. The format is as follows: ```stmpl speakeasy-sdk/csharp {{SDKVersion}} {{GenVersion}} {{DocVersion}} {{PackageName}} ``` - `SDKVersion` is the version of the SDK, defined in `gen.yaml` and released. - `GenVersion` is the version of the Speakeasy generator. - `DocVersion` is the version of the OpenAPI document. - `PackageName` is the name of the package defined in `gen.yaml`. # Comparison guide: OpenAPI/Swagger C# client generation Source: https://speakeasy.com/docs/sdks/languages/csharp/oss-comparison-csharp import { Table } from "@/mdx/components"; Speakeasy produces idiomatic SDKs in various programming languages, including C#. The Speakeasy approach to SDK generation prioritizes a good developer journey to enable API providers to focus on developing a streamlined experience for users. In this article, we'll compare creating a C# SDK using Speakeasy to creating one using the open-source OpenAPI Generator. The table below is a summary of the comparison:
You can explore the Speakeasy [C# SDK documentation](/docs/languages/csharp/methodology-csharp) for more information. For a detailed technical comparison, read on! ## Installing the CLIs We'll start by installing the Speakeasy CLI and the OpenAPI Generator CLI. ### Installing the Speakeasy CLI You can install the Speakeasy CLI by following the installation instructions [here](/docs/speakeasy-reference/cli/getting-started). After installation, you can check the version to ensure the installation was successful: ```bash speakeasy -v ``` If you encounter any errors, take a look at the [Speakeasy SDK creation documentation](/docs/sdks/create-client-sdks). ### Installing the OpenAPI Generator CLI Install the OpenAPI Generator CLI by running the following command in a terminal: ```bash curl -o openapi-generator-cli.jar https://repo1.maven.org/maven2/org/openapitools/openapi-generator-cli/7.2.0/openapi-generator-cli-7.2.0.jar ``` ## Downloading the Swagger Petstore specification We need an OpenAPI specification YAML file to generate SDKs. We'll use the Swagger Petstore specification, which you can find at [https://petstore3.swagger.io/api/v3/openapi.yaml](https://petstore3.swagger.io/api/v3/openapi.yaml). In a terminal in your working directory, download the file and save it as `petstore.yaml` with the following command: ```bash curl -o petstore.yaml https://petstore3.swagger.io/api/v3/openapi.yaml ``` ## Validating the specification file Let's validate the spec using both the Speakeasy CLI and OpenAPI Generator. ### Validating the Specification File Using Speakeasy Validate the spec with Speakeasy using the following command: ```bash speakeasy validate openapi -s petstore.yaml ``` The Speakeasy validator returns the following: ```bash ╭────────────╮╭───────────────╮╭────────────╮ │ Errors (0) ││ Warnings (10) ││ Hints (72) │ ├────────────┴┘ └┴────────────┴────────────────────────────────────────────────────────────╮ │ │ │ │ Line 250: operation-success-response - operation `updatePetWithForm` must define at least a single │ │ │ `2xx` or `3xx` response │ │ │ │ Line 277: operation-success-response - operation `deletePet` must define at least a single `2xx` or │ │ `3xx` response │ │ │ │ Line 413: operation-success-response - operation `deleteOrder` must define at least a single `2xx` o │ │ r │ │ `3xx` response │ │ │ │ Line 437: operation-success-response - operation `createUser` must define at least a single `2xx` or │ │ `3xx` response │ │ │ │ Line 524: operation-success-response - operation `logoutUser` must define at least a single `2xx` or │ │ `3xx` response │ │ │ │ •• │ └────────────────────────────────────────────────────────────────────────────────────────────────────────┘ ←/→ switch tabs ↑/↓ navigate ↵ inspect esc quit ``` The Speakeasy CLI validation result gives us a handy tool for switching between the errors, warnings, and hints tabs with the option to navigate through the results on each tab. In this instance, Speakeasy generated ten warnings. Let's correct them before continuing. Notice that some of the warnings contain a `default` response. For completeness, we'd like to explicitly return a `200` HTTP response. We'll make the following modifications in the `petstore.yaml` file. When the `updatePetWithForm` operation executes successfully, we expect an HTTP `200` response with the updated `Pet` object to be returned. Insert the following after `responses` on line 250: ```yaml "200": description: successful operation content: application/xml: schema: $ref: "#/components/schemas/Pet" application/json: schema: $ref: "#/components/schemas/Pet" ``` Similarly, following successful `createUser` and `updateUser` operations, we'd like to return an HTTP `200` response with a `User` object. Add the following text to both operations below `responses`: ```yaml "200": description: successful operation content: application/xml: schema: $ref: "#/components/schemas/User" application/json: schema: $ref: "#/components/schemas/User" ``` Now we'll add the same response to four operations. Copy the following text: ```yaml "200": description: successful operation ``` Paste this response after `responses` for the following operations: - `deletePet` - `deleteOrder` - `logoutUser` - `deleteUser` We are left with three warnings indicating potentially unused or orphaned objects and operations. For unused objects, locate the following lines of code and delete them: ```yaml Customer: type: object properties: id: type: integer format: int64 example: 100000 username: type: string example: fehguy address: type: array xml: name: addresses wrapped: true items: $ref: "#/components/schemas/Address" xml: name: customer Address: type: object properties: street: type: string example: 437 Lytton city: type: string example: Palo Alto state: type: string example: CA zip: type: string example: "94301" xml: name: address ``` To remove the unused request bodies, locate the following lines and delete them: ```yaml requestBodies: Pet: description: Pet object that needs to be added to the store content: application/json: schema: $ref: "#/components/schemas/Pet" application/xml: schema: $ref: "#/components/schemas/Pet" UserArray: description: List of user object content: application/json: schema: type: array items: $ref: "#/components/schemas/User" ``` Now if you validate the file with the Speakeasy CLI, you'll notice there are no warnings: ``` ╭────────────╮╭──────────────╮╭────────────╮ │ Errors (0) ││ Warnings (0) ││ Hints (75) │ ├────────────┴┴──────────────┴┘ └─────────────────────────────────────────────────────────────╮ │ │ │ │ Line 51: missing-examples - Missing example for requestBody. Consider adding an example │ │ │ │ Line 54: missing-examples - Missing example for requestBody. Consider adding an example │ │ │ │ Line 57: missing-examples - Missing example for requestBody. Consider adding an example │ │ │ │ Line 65: missing-examples - Missing example for responses. Consider adding an example │ │ │ │ Line 68: missing-examples - Missing example for responses. Consider adding an example │ │ │ │ ••••••••••••••• │ └────────────────────────────────────────────────────────────────────────────────────────────────────────┘ ←/→ switch tabs ↑/↓ navigate ↵ inspect esc quit ``` ### Validating the specification file using the OpenAPI Generator OpenAPI Generator requires Java runtime environment (JRE) version 11 or later installed. Confirm whether JRE is installed on your system by executing the following command: ```bash java --version ``` If Java is installed, information about the version should be displayed similar to this: ```bash java 17.0.8 2023-07-18 LTS Java(TM) SE Runtime Environment (build 17.0.8+9-LTS-211) Java HotSpot(TM) 64-Bit Server VM (build 17.0.8+9-LTS-211, mixed mode, sharing) ``` If you get an error or the JRE version is older than version 11, you need to update or [install Java](https://docs.oracle.com/goldengate/1212/gg-winux/GDRAD/java.htm#BGBFJHAB). Now you can validate the `petstore.yaml` specification file with OpenAPI Generator by running the following command in the terminal: ```bash java -jar openapi-generator-cli.jar validate -i petstore.yaml ``` The OpenAPI Generator returns the following response, indicating no issues detected. ``` Validating spec (petstore.yaml) No validation issues detected. ``` Now that we have made the `petstore.yaml` file more complete by fixing the warnings, let's use it to create SDKs. ## Creating SDKs We'll create C# SDKs using Speakeasy and OpenAPI Generator and then compare them. ### Creating an SDK with Speakeasy To create a C# SDK from the `petstore.yaml` specification file using Speakeasy, run the following command in the terminal: ```bash # Generate Pet store SDK using Speakeasy CLI generator speakeasy quickstart ``` The generator will return some logging results while the SDK is being created and a success indicator should appear upon completion. ``` SDK for csharp generated successfully ✓ ``` ### Creating an SDK with OpenAPI Generator Run the following command in the terminal to generate a C# SDK using OpenAPI Generator: ```bash # Generate Pet store SDK using OpenAPI generator java -jar openapi-generator-cli.jar generate -i petstore.yaml -g csharp -o petstore-sdk-csharp-openapi ``` The generator returns various logs and finally a successful generation message. ``` ################################################################################ # Thanks for using OpenAPI Generator. # # Please consider donation to help us maintain this project ? # # https://opencollective.com/openapi_generator/donate # # # # This generator's contributed by Jim Schubert (https://github.com/jimschubert)# # Please support his work directly via https://patreon.com/jimschubert ? # ################################################################################ ``` ## SDK code compared: Project structure Let's compare the two project structures by printing a tree view of each SDK directory. Run the following command to get the Speakeasy SDK structure: ```bash tree petstore-sdk-csharp-speakeasy ``` The results of the project structure are displayed as follows: ``` petstore-sdk-csharp-speakeasy │ .gitattributes │ .gitignore │ global.json │ Openapi.sln │ README.md │ USAGE.md │ ├───.speakeasy │ gen.lock │ gen.yaml │ ├───docs │ ├───Models │ │ ├───Components │ │ │ ApiResponse.md │ │ │ Category.md │ │ │ HTTPMetadata.md │ │ │ Order.md │ │ │ OrderStatus.md │ │ │ Pet.md │ │ │ Security.md │ │ │ Status.md │ │ │ Tag.md │ │ │ User.md │ │ │ │ │ └───Requests │ │ AddPetFormResponse.md │ │ AddPetJsonResponse.md │ │ AddPetRawResponse.md │ │ CreateUserFormResponse.md │ │ CreateUserJsonResponse.md │ │ CreateUserRawResponse.md │ │ CreateUsersWithListInputResponse.md │ │ DeleteOrderRequest.md │ │ DeleteOrderResponse.md │ │ DeletePetRequest.md │ │ DeletePetResponse.md │ │ DeleteUserRequest.md │ │ DeleteUserResponse.md │ │ FindPetsByStatusRequest.md │ │ FindPetsByStatusResponse.md │ │ FindPetsByTagsRequest.md │ │ FindPetsByTagsResponse.md │ │ GetInventoryResponse.md │ │ GetInventorySecurity.md │ │ GetOrderByIdRequest.md │ │ GetOrderByIdResponse.md │ │ GetPetByIdRequest.md │ │ GetPetByIdResponse.md │ │ GetPetByIdSecurity.md │ │ GetUserByNameRequest.md │ │ GetUserByNameResponse.md │ │ LoginUserRequest.md │ │ LoginUserResponse.md │ │ LogoutUserResponse.md │ │ PlaceOrderFormResponse.md │ │ PlaceOrderJsonResponse.md │ │ PlaceOrderRawResponse.md │ │ Status.md │ │ UpdatePetFormResponse.md │ │ UpdatePetJsonResponse.md │ │ UpdatePetRawResponse.md │ │ UpdatePetWithFormRequest.md │ │ UpdatePetWithFormResponse.md │ │ UpdateUserFormRequest.md │ │ UpdateUserFormResponse.md │ │ UpdateUserJsonRequest.md │ │ UpdateUserJsonResponse.md │ │ UpdateUserRawRequest.md │ │ UpdateUserRawResponse.md │ │ UploadFileRequest.md │ │ UploadFileResponse.md │ │ │ └───sdks │ ├───pet │ │ README.md │ │ │ ├───sdk │ │ README.md │ │ │ ├───store │ │ README.md │ │ │ └───user │ README.md │ └───Openapi │ Openapi.csproj │ Pet.cs │ SDK.cs │ Store.cs │ User.cs │ ├───Hooks │ HookRegistration.cs │ HookTypes.cs │ SDKHooks.cs │ ├───Models │ ├───Components │ │ ApiResponse.cs │ │ Category.cs │ │ HTTPMetadata.cs │ │ Order.cs │ │ OrderStatus.cs │ │ Pet.cs │ │ Security.cs │ │ Status.cs │ │ Tag.cs │ │ User.cs │ │ │ ├───Errors │ │ SDKException.cs │ │ │ └───Requests │ AddPetFormResponse.cs │ AddPetJsonResponse.cs │ AddPetRawResponse.cs │ CreateUserFormResponse.cs │ CreateUserJsonResponse.cs │ CreateUserRawResponse.cs │ CreateUsersWithListInputResponse.cs │ DeleteOrderRequest.cs │ DeleteOrderResponse.cs │ DeletePetRequest.cs │ DeletePetResponse.cs │ DeleteUserRequest.cs │ DeleteUserResponse.cs │ FindPetsByStatusRequest.cs │ FindPetsByStatusResponse.cs │ FindPetsByTagsRequest.cs │ FindPetsByTagsResponse.cs │ GetInventoryResponse.cs │ GetInventorySecurity.cs │ GetOrderByIdRequest.cs │ GetOrderByIdResponse.cs │ GetPetByIdRequest.cs │ GetPetByIdResponse.cs │ GetPetByIdSecurity.cs │ GetUserByNameRequest.cs │ GetUserByNameResponse.cs │ LoginUserRequest.cs │ LoginUserResponse.cs │ LogoutUserResponse.cs │ PlaceOrderFormResponse.cs │ PlaceOrderJsonResponse.cs │ PlaceOrderRawResponse.cs │ Status.cs │ UpdatePetFormResponse.cs │ UpdatePetJsonResponse.cs │ UpdatePetRawResponse.cs │ UpdatePetWithFormRequest.cs │ UpdatePetWithFormResponse.cs │ UpdateUserFormRequest.cs │ UpdateUserFormResponse.cs │ UpdateUserJsonRequest.cs │ UpdateUserJsonResponse.cs │ UpdateUserRawRequest.cs │ UpdateUserRawResponse.cs │ UploadFileRequest.cs │ UploadFileResponse.cs │ └───Utils │ AnyDeserializer.cs │ BigIntStrConverter.cs │ DecimalStrConverter.cs │ EnumConverter.cs │ FlexibleObjectDeserializer.cs │ HeaderSerializer.cs │ IsoDateTimeSerializer.cs │ RequestBodySerializer.cs │ ResponseBodyDeserializer.cs │ SecurityMetadata.cs │ SpeakeasyHttpClient.cs │ SpeakeasyMetadata.cs │ URLBuilder.cs │ Utilities.cs │ └───Retries BackoffStrategy.cs Retries.cs RetryConfig.cs ``` The OpenAPI Generator SDK structure can be created with: ```bash tree petstore-sdk-csharp-openapi ``` The results look like this: ``` petstore-sdk-csharp-openapi │ .gitignore │ .openapi-generator-ignore │ appveyor.yml │ git_push.sh │ Org.OpenAPITools.sln │ README.md │ ├───.openapi-generator │ FILES │ VERSION │ ├───api │ openapi.yaml │ ├───docs │ Address.md │ ApiResponse.md │ Category.md │ Customer.md │ Order.md │ Pet.md │ PetApi.md │ StoreApi.md │ Tag.md │ User.md │ UserApi.md │ └───src ├───Org.OpenAPITools │ │ Org.OpenAPITools.csproj │ │ │ ├───Api │ │ PetApi.cs │ │ StoreApi.cs │ │ UserApi.cs │ │ │ ├───Client │ │ │ ApiClient.cs │ │ │ ApiException.cs │ │ │ ApiResponse.cs │ │ │ ClientUtils.cs │ │ │ Configuration.cs │ │ │ ExceptionFactory.cs │ │ │ GlobalConfiguration.cs │ │ │ HttpMethod.cs │ │ │ IApiAccessor.cs │ │ │ IAsynchronousClient.cs │ │ │ IReadableConfiguration.cs │ │ │ ISynchronousClient.cs │ │ │ Multimap.cs │ │ │ OpenAPIDateConverter.cs │ │ │ RequestOptions.cs │ │ │ RetryConfiguration.cs │ │ │ │ │ └───Auth │ │ OAuthAuthenticator.cs │ │ OAuthFlow.cs │ │ TokenResponse.cs │ │ │ └───Model │ AbstractOpenAPISchema.cs │ Address.cs │ ApiResponse.cs │ Category.cs │ Customer.cs │ Order.cs │ Pet.cs │ Tag.cs │ User.cs │ └───Org.OpenAPITools.Test │ Org.OpenAPITools.Test.csproj │ ├───Api │ PetApiTests.cs │ StoreApiTests.cs │ UserApiTests.cs │ └───Model AddressTests.cs ApiResponseTests.cs CategoryTests.cs CustomerTests.cs OrderTests.cs PetTests.cs TagTests.cs UserTests.cs ``` The Speakeasy-created SDK contains more generated files than the SDK from OpenAPI Generator, which is partly due to the Speakeasy SDK being less dependent on third-party libraries. ## Model and usage The Speakeasy SDK follows an object-oriented approach to constructing model objects, leveraging C# support for object initializers. Here's an example of creating and updating a `Pet` object: ```csharp var sdk = new SDK(); var req = new Models.Components.Pet(); { Id = 10, Name = "doggie", Category = new Category() { Id = 1, Name = "Dogs" }, PhotoUrls = new List() { "" } }; var res = await sdk.Pet.UpdatePetJsonAsync(req); ``` The model classes are defined as structured and type-safe, using C# classes and properties. Object initializer syntax makes it convenient to instantiate and populate model objects. The OpenAPI Generator SDK takes a similar approach to constructing model objects. Here's an example of adding a new `Pet` object: ```csharp // Configure API client var config = new Configuration(); config.BasePath = "/api/v3"; // using traditional constructor // var photo = new List() { // "https://hips.hearstapps.com/hmg-prod/images/dog-puppy-on-garden-royalty-free-image-1586966191.jpg?crop=1xw:0.74975xh;center,top&resize=1200:*" // }; // var cat = new Category(10); // var pet = new Pet(10,"openApiDoggie",cat,photo); // Create an instance of the API class using object initializer var apiInstance = new PetApi(config); try { var pet = new Pet(); { Id = 10, Name = "openAPiDoggie", Category = new Category() { Id = 10 }, PhotoUrls = new List() { "https://hips.hearstapps.com/hmg-prod/images/dog-puppy-on-garden-royalty-free-image-1586966191.jpg?crop=1xw:0.74975xh;center,top&resize=1200:*" }, }; Pet result = apiInstance.AddPet(pet); Console.WriteLine(result.ToString()); } catch (ApiException e) { Debug.Print("Exception when calling PetApi.AddPet: " + e.Message); Debug.Print("Status Code: " + e.ErrorCode); Debug.Print(e.StackTrace); } ``` Model classes are defined using constructors and property setters. While this approach is more verbose, it follows a more traditional style that may be familiar to developers coming from other backgrounds. Note that modern language features in .NET allow classes to be initialized using object initializers too, as shown in the example above. In the Speakeasy SDK, the model object is instantiated and populated using an object initializer, providing a more concise and fluent syntax. The OpenAPI Generator SDK, on the other hand, makes use of constructors and individual property setters, making the code more verbose, but also allowing the use of object initializers. Both SDKs provide mechanisms for handling exceptions and error cases when interacting with the API. ## JSON serialization and deserialization The Speakeasy SDK uses attributes from the `Newtonsoft.Json` library for the JSON serialization and deserialization of objects. ```csharp #nullable enable namespace Openapi.Models.Components { using Newtonsoft.Json; using Openapi.Models.Components; using Openapi.Utils; using System.Collections.Generic; public class Pet { [JsonProperty("id")] [SpeakeasyMetadata("form:name=id")] public long? Id { get; set; } [JsonProperty("name")] [SpeakeasyMetadata("form:name=name")] public string Name { get; set; } = default!; [JsonProperty("category")] [SpeakeasyMetadata("form:name=category,json")] public Category? Category { get; set; } [JsonProperty("photoUrls")] [SpeakeasyMetadata("form:name=photoUrls")] public List PhotoUrls { get; set; } = default!; [JsonProperty("tags")] [SpeakeasyMetadata("form:name=tags,json")] public List? Tags { get; set; } /// /// pet status in the store /// [JsonProperty("status")] [SpeakeasyMetadata("form:name=status")] public Models.Components.Status? Status { get; set; } } } ``` The `JsonProperty` attribute is used to map class properties to their corresponding JSON fields. The `SpeakeasyMetadata` attribute is used to provide additional metadata for form encoding and other purposes. By contrast, the OpenAPI Generator SDK uses the `Newtonsoft.Json.Converters` namespace for JSON serialization and deserialization: ```csharp /// /// Pet /// [DataContract(Name = "Pet")] public partial class Pet : IValidatableObject { /// /// pet status in the store /// /// pet status in the store [JsonConverter(typeof(StringEnumConverter))] public enum StatusEnum { /// /// Enum Available for value: available /// [EnumMember(Value = "available")] Available = 1, /// /// Enum Pending for value: pending /// [EnumMember(Value = "pending")] Pending = 2, /// /// Enum Sold for value: sold /// [EnumMember(Value = "sold")] Sold = 3 } /// /// pet status in the store /// /// pet status in the store [DataMember(Name = "status", EmitDefaultValue = false)] public StatusEnum? Status { get; set; } /// /// Initializes a new instance of the class. /// [JsonConstructorAttribute] protected Pet() { } /// /// Initializes a new instance of the class. /// /// id. /// name (required). /// category. /// photoUrls (required). /// tags. /// pet status in the store. public Pet(long id = default(long), string name = default(string), Category category = default(Category), List photoUrls = default(List), List tags = default(List), StatusEnum? status = default(StatusEnum?)) { // to ensure "name" is required (not null) if (name == null) { throw new ArgumentNullException("name is a required property for Pet and cannot be null"); } this.Name = name; // to ensure "photoUrls" is required (not null) if (photoUrls == null) { throw new ArgumentNullException("photoUrls is a required property for Pet and cannot be null"); } this.PhotoUrls = photoUrls; this.Id = id; this.Category = category; this.Tags = tags; this.Status = status; } /// /// Gets or Sets Id /// /// 10 [DataMember(Name = "id", EmitDefaultValue = false)] public long Id { get; set; } /// /// Gets or Sets Name /// /// doggie [DataMember(Name = "name", IsRequired = true, EmitDefaultValue = true)] public string Name { get; set; } } ``` The OpenAPI Generator SDK attempts to use default values in the constructor to handle nullable types by forcing default values. The `DataContract` and `DataMember` annotations from the `System.Runtime.Serialization` namespace specify which properties of a class should be included during serialization and deserialization. While both SDKs use the `Newtonsoft.Json` library, the Speakeasy SDK takes a more straightforward approach by directly using the `JsonProperty` attribute. The OpenAPI Generator SDK relies on `DataContract` and `DataMember` for the process. ## Model implementation The Speakeasy SDK uses a more modern approach to defining model classes. Here's the `Pet` class implementation: ```csharp #nullable enable namespace Openapi.Models.Components { using Newtonsoft.Json; using Openapi.Utils; using System.Collections.Generic; public class Pet { [JsonProperty("id")] [SpeakeasyMetadata("form:name=id")] public long? Id { get; set; } [JsonProperty("name")] [SpeakeasyMetadata("form:name=name")] public string Name { get; set; } = default!; [JsonProperty("category")] [SpeakeasyMetadata("form:name=category,json")] public Category? Category { get; set; } [JsonProperty("photoUrls")] [SpeakeasyMetadata("form:name=photoUrls")] public List PhotoUrls { get; set; } = default!; [JsonProperty("tags")] [SpeakeasyMetadata("form:name=tags,json")] public List? Tags { get; set; } /// /// pet status in the store /// [JsonProperty("status")] [SpeakeasyMetadata("form:name=status")] public Models.Components.Status? Status { get; set; } } } ``` The Speakeasy SDK model class definitions follow a property-based approach, using properties decorated with `JsonProperty` attributes for JSON serialization and deserialization, and `SpeakeasyMetadata` attributes for additional metadata. Null safety is ensured by the `#nullable enable` directive, and nullable types like `long?` and non-null default values like `= default!` help to prevent unexpected `NullReferenceException` issues. By contrast, the OpenAPI Generator SDK's `Pet` class has a more traditional implementation: ```csharp /// /// Pet /// [DataContract(Name = "Pet")] public partial class Pet : IValidatableObject { /// /// pet status in the store /// /// pet status in the store [JsonConverter(typeof(StringEnumConverter))] public enum StatusEnum { /// /// Enum Available for value: available /// [EnumMember(Value = "available")] Available = 1, } /// /// pet status in the store /// /// pet status in the store [DataMember(Name = "status", EmitDefaultValue = false)] public StatusEnum? Status { get; set; } public Pet(long id = default(long), string name = default(string), Category category = default(Category), List photoUrls = default(List), List tags = default(List), StatusEnum? status = default(StatusEnum?)) { // to ensure "name" is required (not null) if (name == null) { throw new ArgumentNullException("name is a required property for Pet and cannot be null"); } this.Name = name; // to ensure "photoUrls" is required (not null) if (photoUrls == null) { throw new ArgumentNullException("photoUrls is a required property for Pet and cannot be null"); } this.PhotoUrls = photoUrls; this.Id = id; this.Category = category; this.Tags = tags; this.Status = status; } /// /// Returns the JSON string presentation of the object /// /// JSON string presentation of the object public virtual string ToJson() { return Newtonsoft.Json.JsonConvert.SerializeObject(this, Newtonsoft.Json.Formatting.Indented); } /// /// To validate all properties of the instance /// /// Validation context /// Validation Result IEnumerable IValidatableObject.Validate(ValidationContext validationContext) { yield break; } } ``` The OpenAPI Generator SDK uses data contracts and attributes from the `System.Runtime.Serialization` namespace for serialization and deserialization, and includes additional `ToString()`, `ToJson()`, and `Validate()` methods. The `StatusEnum` property is implemented as a separate enum, which adds complexity to the model class. The Speakeasy SDK model implementation is more concise and follows a modern and idiomatic approach to defining C# classes. The OpenAPI Generator SDK model implementation is more verbose and includes traditional features like validation and string representation methods. ## HTTP communication The Speakeasy SDK handles HTTP communication using the `System.Net.Http` namespace, which is part of the .NET Base Class Library (BCL). Here's an example of the `AddPetJsonAsync` method from the `Pet` class: ```csharp public async Task AddPetJsonAsync(Models.Components.Pet request) { string baseUrl = this.SDKConfiguration.GetTemplatedServerUrl(); var urlString = baseUrl + "/pet"; var httpRequest = new HttpRequestMessage(HttpMethod.Post, urlString); httpRequest.Headers.Add("user-agent", _userAgent); var serializedBody = RequestBodySerializer.Serialize(request, "Request", "json", false, false); if (serializedBody != null) { httpRequest.Content = serializedBody; } HttpResponseMessage httpResponse; try { httpResponse = await _client.SendAsync(httpRequest); // ... (handle response and exceptions) } catch (Exception error) { // ... (handle exceptions) } // ... (additional processing and return response) } ``` The `AddPetJsonAsync` method constructs a `HttpRequestMessage` object with the appropriate method and URL. It then serializes the request body, sets the necessary headers, and applies security and hooks. It sends the request using the `SendAsync` method, which returns an `HttpResponseMessage`. The OpenAPI Generator SDK has a more complicated approach to HTTP communication, defining several custom classes and types to manage the process. Ultimately, it relies on the `RestSharp` library and `RestSharp.Serializers` for executing the HTTP requests and handling serialization. Here's the `AddPetAsync` method from the `PetApi` class: ```csharp public async System.Threading.Tasks.Task AddPetAsync(Pet pet, int operationIndex = 0, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) { Org.OpenAPITools.Client.ApiResponse localVarResponse = await AddPetWithHttpInfoAsync(pet, operationIndex, cancellationToken).ConfigureAwait(false); return localVarResponse.Data; } ``` The `AddPetAsync` method calls the `AddPetWithHttpInfoAsync` method, which handles the HTTP communication details. To make the HTTP request and process the response, the SDK uses a custom `AsynchronousClient` class, which internally uses the third-party `RestSharp` library for HTTPS communication. Both SDKs leverage async/await for asynchronous operations, but the Speakeasy SDK takes advantage of the built-in `System.Net.Http` namespace in .NET, providing a more integrated and efficient approach to HTTP communication. The custom HTTP communication implementation of the OpenAPI Generator SDK depends on a third-party library, which brings additional maintenance and compatibility considerations. ## Retries The Speakeasy SDK provides built-in support for automatically retrying failed requests. You can configure retries globally or on a per-request basis using the `x-speakeasy-retries` extension in your OpenAPI specification document. Here's how the `AddPetJsonAsync` method handles retries: ```csharp public async Task AddPetJsonAsync(Models.Components.Pet request, RetryConfig? retryConfig = null) { if (retryConfig == null) { if (this.SDKConfiguration.RetryConfig != null) { retryConfig = this.SDKConfiguration.RetryConfig; } else { var backoff = new BackoffStrategy( initialIntervalMs: 500L, maxIntervalMs: 60000L, maxElapsedTimeMs: 3600000L, exponent: 1.5 ); retryConfig = new RetryConfig( strategy: RetryConfig.RetryStrategy.BACKOFF, backoff: backoff, retryConnectionErrors: true ); } } List statusCodes = new List { "5XX", }; Func> retrySend = async () => { var _httpRequest = await _client.CloneAsync(httpRequest); return await _client.SendAsync(_httpRequest); }; var retries = new Openapi.Utils.Retries.Retries(retrySend, retryConfig, statusCodes); HttpResponseMessage httpResponse; try { httpResponse = await retries.Run(); // ... (handle response and exceptions) } catch (Exception error) { // ... (handle exceptions) } // ... (additional processing and return response) } ``` If no `RetryConfig` is provided, the method checks for a global `RetryConfig` in the `SDKConfiguration`. If no global `RetryConfig` is found, a default `BackoffStrategy` is created with values for initial interval, maximum interval, maximum elapsed time, and exponential backoff factor. The `retrySend` function clones the original `HttpRequestMessage` prior to sending. This prevents it from being consumed by the `SendAsync` method, enabling subsequent resends. An instance of the `Retries` class is created, taking the `retrySend` function, `retryConfig`, and status codes as arguments. The `retries.Run()` method is then called to handle the entire retry logic and it returns the final `HttpResponseMessage`. Various retry strategies, like backoff or fixed interval, are supported and most options are configurable. The `x-speakeasy-retries` extension can be used in an OpenAPI specification file to configure retries for specific operations or globally. For more information on configuring retries in your SDK, take a look at the [retries documentation](/docs/customize-sdks/retries). The OpenAPI Generator SDK does not provide built-in support for automatic retries, and you would need to implement this functionality manually or by using a third-party library. ### SDK dependencies The Speakeasy SDK has the following external dependencies: ```xml ``` - **Newtonsoft.Json:** A JSON framework for .NET used for JSON serialization and deserialization. - **Noda Time:** A date and time API for .NET, providing a better implementation than the built-in `System.DateTime` components. The OpenAPI Generator SDK has the following external dependencies: - **JsonSubTypes:** A library used for handling JSON polymorphism, useful for dealing with inheritance hierarchies in JSON data. - **Newtonsoft.Json:** a JSON framework for .NET, which is used for JSON serialization and deserialization in the SDK. - **RestSharp:** A library for consuming RESTful web services in .NET, used for making HTTP requests and handling responses. - **Polly:** A .NET resilience and transient-fault-handling library that allows developers to express policies such as Retry, Circuit Breaker, Timeout, Bulkhead Isolation, and Fallback in a fluent and thread-safe way. The OpenAPI Generator SDK project file includes a reference to the `System.Web` assembly, which is part of the .NET Framework and provides classes for building web applications. While both SDKs use the Newtonsoft.Json library for JSON handling, the Speakeasy SDK has a more minimalistic approach to dependencies and only includes the `NodaTime` library for date and time handling. The OpenAPI Generator SDK includes additional dependencies like `RestSharp` for HTTP communication, `JsonSubTypes` for JSON polymorphism, and `Polly` for resilience and fault handling. The more dependencies an SDK has, the more prone it is to compatibility issues with new releases and internal complexity, making maintenance and enhancement more difficult. ## Handling non-nullable fields Let's compare how the two SDKs handle non-nullable fields using the provided code snippets for the `Status` field and enum. In the Speakeasy SDK the `Status` field is defined as follows: ```csharp /// /// pet status in the store /// [JsonProperty("status")] [SpeakeasyMetadata("form:name=status")] public Models.Components.Status? Status { get; set; } ``` The `Status` property is of type `Models.Components.Status?`, which is a nullable enum type. The `?` after the type name indicates that the property can be assigned a `null` value. The `Status` enum is defined as follows: ```csharp public enum Status { [JsonProperty("available")] Available, [JsonProperty("pending")] Pending, [JsonProperty("sold")] Sold } ``` The enum members are decorated with the `JsonProperty` attribute, which specifies the JSON property name for each member. In the OpenAPI Generator SDK, the `Status` field is defined as follows: ```csharp /// /// pet status in the store /// /// pet status in the store [DataMember(Name = "status", EmitDefaultValue = false)] public StatusEnum? Status { get; set; } ``` The `Status` property is of type `StatusEnum?`, which is a nullable enum type. The `StatusEnum` is defined as follows: ```csharp /// /// pet status in the store /// /// pet status in the store [JsonConverter(typeof(StringEnumConverter))] public enum StatusEnum { /// /// Enum Available for value: available /// [EnumMember(Value = "available")] Available = 1, /// /// Enum Pending for value: pending /// [EnumMember(Value = "pending")] Pending = 2, /// /// Enum Sold for value: sold /// [EnumMember(Value = "sold")] Sold = 3 } ``` The `StatusEnum` is decorated with the `JsonConverter` attribute, which specifies that the `StringEnumConverter` should be used for JSON serialization and deserialization. The enum members are decorated with the `EnumMember` attribute, which specifies the JSON value for each member. Both SDKs handle non-nullable fields similarly by using nullable types (`Status?` and `StatusEnum?`), allowing the SDK to accommodate scenarios where the API response may not include a value for the `Status` field. The SDKs differ in how the enums are defined and decorated with attributes for JSON serialization and deserialization: - The Speakeasy SDK uses the `JsonProperty` attribute directly on the enum members to specify the JSON property name. - The OpenAPI Generator SDK uses the `JsonConverter` and `EnumMember` attributes to handle JSON serialization and deserialization for the enum. The same goal is achieved in both cases, but the Speakeasy approach is more straightforward, as it directly maps the enum members to the corresponding JSON property names. Let's look at how the two SDKs handle this when passing a null value to the `FindPetsByStatus` method. When you run the following code from the Speakeasy SDK: ```csharp try { var res = await sdk.Pet.FindPetsByStatusAsync(null); if (res.Body != null) { //handle response } } catch (Exception ex) { Console.WriteLine(ex.Message + "\\n" + ex.StackTrace); } ``` The Speakeasy SDK throws an exception with the following output: ``` API error occurred at Openapi.Pet.FindPetsByStatusAsync(Nullable`1 status) in C:\Users\devi\Documents\git\speak-easy\sdks\petstore-sdk-csharp-speakeasy\Openapi\Pet.cs:line 852 at Program.
$(String[] args) in C:\Users\devi\Documents\git\speak-easy\sdks\petstore-sdk-csharp-speakeasy\conSpeakEasyTester\Program.cs:line 11 ``` The Speakeasy SDK throws an `SDKException` with the message "API error occurred" when encountering an error during the API call. It also includes the stack trace, which can be helpful for debugging purposes. When you run the following code from the OpenAPI Generator SDK: ```csharp try { // Add a new pet to the store // Pet result = apiInstance.AddPet(pet); var res = apiInstance.FindPetsByStatus(null); Debug.WriteLine(res); } catch (ApiException e) { Console.WriteLine("Exception when calling PetApi.AddPet: " + e.Message); Console.WriteLine("Status Code: " + e.ErrorCode); Console.WriteLine(e.StackTrace); } ``` The OpenAPI Generator SDK throws an `ApiException` with the following output: ``` Org.OpenAPITools.Client.ApiException: 'Error calling FindPetsByStatus: No status provided. Try again?' ``` The OpenAPI Generator SDK throws an `ApiException` with a more descriptive error message, but it does not include the stack trace by default. Both SDKs handle the null value scenario by throwing an exception, which is a reasonable approach to prevent invalid data from being passed to the API. The Speakeasy SDK throws a more generic "API error occurred" exception but provides the stack trace, which can be helpful for debugging. The OpenAPI Generator SDK throws a more descriptive `ApiException` with a customized error message, but it does not include the stack trace by default. Let's see what happens when we pass an empty name field to the SDKs. If we remove the name field from the class initialization or even set it to `null`, the Speakeasy SDK doesn't throw an error and creates a pet object with empty or null values provided. To show the details of the pet object created, let's add a method to the `Pet` model class in `sdks\OpenApi\Models\Components\Pet.cs`: ```csharp public override string ToString() { string categoryString = Category != null ? Category.ToString() : "null"; string photoUrlsString = PhotoUrls != null ? string.Join(", ", PhotoUrls) : "null"; string tagsString = Tags != null ? string.Join(", ", Tags) : "null"; string statusString = Status != null ? Status.ToString() : "null"; return $"Pet:\n" + $" Id: {Id}\n" + $" Name: {Name}\n" + $" Category: {categoryString}\n" + $" PhotoUrls: {photoUrlsString}\n" + $" Tags: {tagsString}\n" + $" Status: {statusString}"; } ``` Now if you run the following code: ```csharp var req = new Openapi.Models.Components.Pet() { Id = 10, Name = null, Category = new Category() { Id = 1 }, PhotoUrls = new List() { "https://hips.hearstapps.com/hmg-prod/images/dog-puppy-on-garden-royalty-free-image-1586966191.jpg?crop=1xw:0.74975xh;center,top&resize=1200:*" }, }; Console.WriteLine(req.ToString()); ``` You get the following result, showing that the pet object was created without a value for the `Name` field: ``` Pet: Id: 10 Name: Category: Openapi.Models.Components.Category PhotoUrls: https://hips.hearstapps.com/hmg-prod/images/dog-puppy-on-garden-royalty-free-image-1586966191.jpg?crop=1xw:0.74975xh;center,top&resize=1200:* Tags: null Status: null ``` Let's do the same with the OpenAPI Generator SDK: ```csharp var pet = new Pet() { Id = 10, Name = null, Category = new Category() { Id = 10 }, PhotoUrls = new List() { "https://hips.hearstapps.com/hmg-prod/images/dog-puppy-on-garden-royalty-free-image-1586966191.jpg?crop=1xw:0.74975xh;center,top&resize=1200:*" }, }; Pet result = apiInstance.AddPet(pet); Console.WriteLine(result.ToString()); ``` The OpenAPI Generator SDK throws an `ArgumentNullException` error with a more descriptive error message: ``` System.ArgumentNullException: 'Value cannot be null. (Parameter 'name is a required property for Pet and cannot be null')' ``` It appears that the error is thrown from the model class directly, so we cannot continue with bad data. In this case, the OpenAPI Generator SDK handled the null or empty values better than the Speakeasy SDK when creating a Pet. The Speakeasy SDK allows you to create the pet with empty name values, a small issue that can be handled in development, but worth taking note of. ## Generated documentation Both Speakeasy and OpenAPI Generator create SDK documentation for generated code. The OpenAPI Generator README outlines the SDK dependencies and supported frameworks, provides steps for getting started (including installing dependencies and building the project in various operating systems), and describes available API routes. The Speakeasy README also provides API routes and includes more detailed getting-started examples. OpenAPI Generator generates some documentation in a docs directory, but it is not very detailed. Additional documentation generated by Speakeasy includes more detailed explanations of the models and operations; examples of creating, updating, and searching objects; error handling; and guidance on handling exceptions specific to the OpenAPI specification file. Some default test cases are created for both but are only for guidance. ## Supported .NET versions The Speakeasy-generated SDK supports .NET 5.+ environments. We successfully tested it with .NET 6. The SDK generated by OpenAPI Generator claims to support a range of versions, including .NET Framework 4.7, .NET Standard 1.3-2.1, and .NET 6 and later. We targeted .NET 6 to ensure we have the same language features available in both SDKs. Although more versions are supported in the OpenAPI Generator, .NET 5+ is the modern stack and will be used more in new developments. ## Summary Compared to the SDK generated by OpenAPI Generator, the Speakeasy-generated SDK is lightweight, concise, and idiomatic, with a modern approach to model implementation and built-in retry support. The Speakeasy generator uses modern techniques that follow best practices, and the Speakeasy documentation makes it easy to get started. If you are building an API that developers rely on and would like to publish full-featured SDKs that follow best practices, give the Speakeasy SDK generator a try. [Join our Slack community](https://go.speakeasy.com/slack) to let us know how we can improve our C# SDK generator or suggest features. # Go Feature Reference Source: https://speakeasy.com/docs/sdks/languages/golang/feature-support import { Table } from "@/mdx/components"; ## Authentication
## Server Configuration
## Data Types ### Basic Types
### Polymorphism
## Methods
## Parameters
### Path Parameters Serialization
### Query Parameters Serialization
## Requests
## Responses
## Documentation
# Create Go SDKs From OpenAPI (Swagger) Source: https://speakeasy.com/docs/sdks/languages/golang/methodology-go import { FileTree } from "nextra/components"; ## SDK Overview Speakeasy's Go SDK is designed to build idiomatic Go modules and uses language-standard features. The SDK is backward compatible, requiring only Go 1.14, and will work with all newer compilers. The SDK is strongly typed, makes minimal use of third-party modules, and is straightforward to debug. Speakeasy-generated SDKs should feel familiar to Go developers. Some of the Speakeasy Go SDK design choices are opinionated in a thoughtful and deliberate way. For example, many Go developers prefer to rely on zero values rather than pointers for unset optional values. However, as many REST APIs have nuanced distinctions between zero and null values, this approach is not generally practical for interoperability. To balance this, Speakeasy-created Go SDKs use pointers but include `nil`-safe getters to help offset the increased risk of panics caused by mishandled pointers. Core features of the Speakeasy Go SDK include: - Struct field tags and reflection-based marshaling and unmarshaling of data. - Pointers used for optional fields. - Automatic getters that provide `nil`-safety and hooks for building interfaces. - Context-aware method calls for programmatic timeouts and cancelation. - A `utils` package that segments off common operations, making generated code easier to follow and understand. - Variadic options functions are provided to simplify the construction process, whether you have many options or none. - Authentication support for OAuth flows and standard security mechanisms like HTTP Basic and application tokens. - Optional pagination support for supported APIs. - Optional support for retries in every operation. - Complex number types - `"github.com/ericlager/decimal".Big` - `"math/big".Int` - Date and time types using RFC 3339 formats. - Custom type enums using strings and ints. - Union types and combined types. - Go module vendoring support for dependency management. The SDK includes minimal dependencies. The only third-party dependencies are: - `github.com/ericlagergren/decimal` - providing big decimal support features. - `github.com/cenkalti/backoff/v4` - implementing automatic retry support. - `github.com/spyzhov/ajson` - to help implement pagination. ## Go Package Structure ## HTTP Client The Go SDK makes API calls that wrap an internal HTTP client. The requirements for the HTTP client are simple. It must match this interface: ```go type HTTPClient interface { Do(req *http.Request) (*http.Response, error) } ``` The built-in `net/http` client satisfies this interface and a default client based on the built-in is provided by default. To replace this default with a custom client, implement this interface or provide a configured client as needed. Here's a simple example that adds a client with a 30-second timeout. ```go import ( "net/http" "time" "github.com/myorg/go-sdk" ) var ( httpClient = &http.Client{Timeout: 30 * time.Second} sdkClient = sdk.New(sdk.WithClient(httpClient)) ) ``` This can be a convenient way to configure timeouts, cookies, proxies, custom headers, and other low-level configuration. ## Go Client Data Types and Enums The Speakeasy Go SDK has a strong preference for familiar built-in types. Because the Go language has a rich built-in type system, the Speakeasy Go SDK relies almost completely on it. Here is a list of types the SDK uses: - `string` - `time.Time` - `int` - `int64` - `big.Int` - `float32` - `float64` - `bool` Speakeasy provides a few custom types in the `types` package, which aid with marshaling and unmarshaling data exchanged with the server-side API. For example, `types.Date` is a thin wrapper around `time.Time` that can decode and encode dates in the `"2006-01-02"` format. Speakeasy also uses the `decimal.Big` class provided by `github.com/ericlagergren/decimal`. This is a better alternative to `big.Float`, as it provides high-precision floating point math that avoids the rounding errors that can sometimes occur with `big.Float`. ## Pointer Types for Optional Fields When a field is optional or nullable in OpenAPI, Speakeasy uses pointer types in Go to represent these fields. This design choice allows for a clear distinction between unset values (nil) and zero values. For example: ```go type Pet struct { Name string `json:"name"` // required field ChipID *string `json:"chip_id"` // optional/nullable field } ``` When serializing a Pet struct to JSON, a nil pointer will be correctly represented as `null`: ```go pet := Pet{Name: "Finn"} // ChipID is nil // Serializes to: {"name": "Finn", "chip_id": null} ``` To make working with pointer types more ergonomic, Speakeasy provides helper functions: ```go // Helper function for string pointers func String(s string) *string { return &s } // Generic helper for any type func Pointer[T any](v T) *T { return &v } ``` These helpers allow for cleaner initialization of structs with pointer fields: ```go pet := Pet{ Name: "Finn", ChipID: Pointer("173105fe2"), } ``` This approach eliminates the need for temporary variables when setting pointer fields while maintaining type safety and proper null handling. Enumeration types are built according to typical Go practices. Speakeasy defines a type alias to `string`, `int`, or `int64` as appropriate. Constants of this type are defined for the predefined values. Note that these are true type aliases (using `type X = string`) rather than new types (using `type X string`), meaning they are interchangeable with their underlying types. ## Go SDK Generated Classes The Go SDK generates a `struct` for each request and response object and each component object. All fields in the `struct` objects are public. Optional fields are given pointer types and may be set to `nil`. A getter method is also defined for each public field. The `Get` prefix distinguishes the getters from the public field names, which remain directly accessible. The getters work correctly even when called on a `nil` value, in which case they return the zero value of the field. For example, the following code shows a nested component object where the inner object is optional, ensuring safety from `nil` pointer-related panics. ```go var outer *shared.Outer var safe string = outer.GetInner().GetName() if safe == "" { fmt.Println("Don't Panic") } // output: Don't Panic ``` The getters also provide useful hooks for defining interfaces. ## Parameters As described above, the Speakeasy SDK will generate a class with public fields for each request and response object. Each field will be tagged to control marshaling and unmarshaling into other data formats while interacting with the underlying API. However, if the `maxMethodParams` value is set in `gen.yaml`, the generated struct will be limited to that number of parameters. These parameters will be positioned in the operation method after the context object and before the request object. ```go // maxMethodParams: 1 res, err := sdk.GetDrink(ctx, "Sangria") if err != nil { return err } // work with res... ``` Compare this with the example in the next section where `maxMethodParams` is `0`. ## Errors Following Go best practices, all operation methods in the Speakeasy SDK will return a response object and an error. Callers should always check for the presence of the error. The object used for errors is configurable per request. Any error response may return a custom error object. A generic error will be provided when any communication failure is detected during an operation. Here's an example of custom error handling in a theoretical SDK: ```go longTea := operations.GetDrinkRequest{Name: "Long Island Iced Tea"} res, err := sdk.GetDrink(ctx, &longTea) var apiErr sdkerrors.APIError if errors.As(err, &apiErr) { return fmt.Errorf("failed to get drink (%d): %s", apiErr.GetCode(), apiErr.GetMessage()) } else if err != nil { return fmt.Errorf("unknown error getting drink: %w", err) } // work with res... ``` ## User Agent Strings The Go SDK will include a [user agent](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/User-Agent) string in all requests. This can be leveraged to track SDK usage amongst broader API usage. The format is as follows: ```stmpl speakeasy-sdk/go {{SDKVersion}} {{GenVersion}} {{DocVersion}} {{PackageName}} ``` - `SDKVersion` is the version of the SDK, defined in `gen.yaml` and released. - `GenVersion` is the version of the Speakeasy generator. - `DocVersion` is the version of the OpenAPI document. - `PackageName` is the name of the package defined in `gen.yaml`. # Comparison guide: OpenAPI/Swagger Go client generation Source: https://speakeasy.com/docs/sdks/languages/golang/oss-comparison-go import { Table } from "@/mdx/components"; Speakeasy generates idiomatic Go SDKs based on OpenAPI specifications. In this post, we'll take a look at why many of our users choose to switch from OpenAPI Generate and other open-source generators to Speakeasy to generate their Go SDKs. Open-source SDK generators play an important role in experimentation and smaller custom integrations but teams should publish high-quality SDKs for their APIs that offer the best developer experience. Usable SDKs drive adoption by making it easier for developers to switch to the API. At Speakeasy, we generate idiomatic client SDKs in a variety of languages. Our generators follow principles that ensure we generate SDKs that offer the best developer experience so that you can focus on building your API, and your developer-users can focus on delighting their users. ## Go SDK generator options We'll compare four Go SDK generators: 1. The [Go generator](https://openapi-generator.tech/docs/generators/go/) from the [OpenAPI Generator](https://openapi-generator.tech/) project. 2. [oapi-codegen](https://github.com/oapi-codegen/oapi-codegen), an open-source OpenAPI Client and Server Code Generator. 3. [ogen](https://ogen.dev/), an open-source OpenAPI v3 code generator for Go. 4. The [Speakeasy SDK generator](/docs/speakeasy-reference/cli/getting-started). Below is the summary of how the four evaluated generators compare. Our recommendation is to use Speakeasy for generating Go SDKs for your APIs (1st SDK free). If you are committed to using an open source generator, we strongly recommend that you use [oapi-codegen](https://github.com/oapi-codegen/oapi-codegen)
If you want the detailed technical breakdown, full of code comparisons, read on! ## Installing SDK generators To start our comparison, we installed all four generators on a local machine running macOS. ### Installing the OpenAPI Generator CLI OpenAPI Generator depends on Java, [which we covered at length previously](/post/speakeasy-oss-python-generator#our-experience-installing-the-openapi-generator-cli). We concluded that managing the OpenAPI Generator Java dependencies manually just wasn't worth the effort. Installing `openapi-generator` using Homebrew installs `openjdk@11` and its many dependencies: ```bash brew install openapi-generator ``` This adds the `openapi-generator` CLI to our path. ### Installing oapi-codegen We install oapi-codegen using the Go package manager: ```bash go install github.com/deepmap/oapi-codegen/cmd/oapi-codegen@latest ``` This command installs the oapi-codegen Go module and [its dependencies](https://github.com/deepmap/oapi-codegen/blob/master/go.mod). ### Installing ogen We followed the [ogen quick start](https://ogen.dev/docs/intro) to install ogen: ```bash go install -v github.com/ogen-go/ogen/cmd/ogen@latest ``` This installs the ogen Go module with [its dependencies](https://github.com/ogen-go/ogen/blob/main/go.mod). ### How to install the Speakeasy CLI To install the Speakeasy CLI, follow the steps in the [Speakeasy Getting Started](/docs/speakeasy-reference/cli/getting-started) guide. In the terminal, run: ```bash brew install speakeasy-api/tap/speakeasy ``` Next, authenticate with Speakeasy by running the following: ```bash speakeasy auth login ``` This installs the Speakeasy CLI as a single binary without any dependencies. ## Downloading the Swagger Petstore specification Before we run our generators, we'll need an OpenAPI specification to generate a Go SDK for. The standard specification for testing OpenAPI SDK generators and Swagger UI generators is the [Swagger Petstore](https://petstore3.swagger.io/). We'll download the YAML specification at [https://petstore3.swagger.io/api/v3/openapi.yaml](https://petstore3.swagger.io/api/v3/openapi.yaml) to our working directory and name it `petstore.yaml`: ```bash curl https://petstore3.swagger.io/api/v3/openapi.yaml --output petstore.yaml ``` ## Validating the Spec Both the OpenAPI Generator and Speakeasy CLI can validate an OpenAPI spec. We'll run both and compare the output. ### Validation using OpenAPI Generator To validate `petstore.yaml` using OpenAPI Generator, run the following in the terminal: ```bash openapi-generator validate -i petstore.yaml ``` The OpenAPI Generator returns two warnings: ``` Warnings: - Unused model: Address - Unused model: Customer [info] Spec has 2 recommendation(s). ``` ### Validation using Speakeasy We'll validate the spec with Speakeasy by running the following in the terminal: ```bash speakeasy validate openapi -s petstore.yaml ``` The Speakeasy validator returns ten warnings, seven hints that some methods don't specify any return values and three unused components. Each warning includes a detailed JSON-formatted error with line numbers. Since both validators validated the spec with only warnings, we can assume that both will generate SDKs without issues. ## Generating an SDK Now that we know our OpenAPI spec is valid, we can start generating SDKs. We'll generate each SDK in a unique subdirectory, relative to where we saved the `petstore.yaml` spec file. ### OpenAPI generate OpenAPI Generator features SDK generators for multiple languages, often with multiple options per language. We'll test the Go generator in this post. We'll generate an SDK by running the following in the terminal: ```bash # Generate Petstore SDK using go generator openapi-generator generate \ --input-spec petstore.yaml \ --generator-name go \ --output ./petstore-sdk-go-openapi ``` This command will print a list of files generated and populate the new `petstore-sdk-go-openapi` directory. ### Generating an SDK using oapi-codegen Before we generate an SDK using oapi-codegen, we'll need to create a new directory for this SDK. Run the following in the terminal: ```bash mkdir petstore-sdk-go-oapi-codegen && cd petstore-sdk-go-oapi-codegen ``` Create a Go module in the new directory: ```bash go mod init petstore-sdk-go-oapi-codegen ``` Then run the oapi-codegen Go module: ```bash go run github.com/deepmap/oapi-codegen/cmd/oapi-codegen@latest -package petstore ../petstore.yaml > petstore.gen.go ``` ### Generating an SDK using ogen We followed the ogen quick start documentation. Create a new directory for our ogen SDK and navigate to it in the terminal: ```bash mkdir petstore-sdk-go-ogen && cd petstore-sdk-go-ogen ``` Create a new Go module in this directory: ```bash go mod init petstore-sdk-go-ogen ``` Copy the `petstore.yaml` spec into this directory: ```bash cp ../petstore.yaml . ``` Create a new file called `generate.go` with the following contents: ```go package project //go:generate go run github.com/ogen-go/ogen/cmd/ogen@latest --target petstore --clean --no-server petstore.yaml ``` Then run the following from our new directory: ```bash go generate ./... ``` In our testing, this resulted in a stack trace and returned an error status: ``` INFO convenient Convenient errors are not available {"reason": "operation has no \"default\" response", "at": "petstore.yaml:59:9"} generate: main.run /Users/ritza/go/pkg/mod/github.com/ogen-go/ogen@v0.69.1/cmd/ogen/main.go:304 - build IR: main.generate /Users/ritza/go/pkg/mod/github.com/ogen-go/ogen@v0.69.1/cmd/ogen/main.go:64 - make ir: github.com/ogen-go/ogen/gen.NewGenerator /Users/ritza/go/pkg/mod/github.com/ogen-go/ogen@v0.69.1/gen/generator.go:112 - operations: github.com/ogen-go/ogen/gen.(*Generator).makeIR /Users/ritza/go/pkg/mod/github.com/ogen-go/ogen@v0.69.1/gen/generator.go:130 - path "/pet": put: github.com/ogen-go/ogen/gen.(*Generator).makeOps /Users/ritza/go/pkg/mod/github.com/ogen-go/ogen@v0.69.1/gen/generator.go:171 - requestBody: github.com/ogen-go/ogen/gen.(*Generator).generateOperation /Users/ritza/go/pkg/mod/github.com/ogen-go/ogen@v0.69.1/gen/gen_operation.go:49 - contents: github.com/ogen-go/ogen/gen.(*Generator).generateRequest /Users/ritza/go/pkg/mod/github.com/ogen-go/ogen@v0.69.1/gen/gen_request_body.go:27 - media: "application/x-www-form-urlencoded": github.com/ogen-go/ogen/gen.(*Generator).generateContents /Users/ritza/go/pkg/mod/github.com/ogen-go/ogen@v0.69.1/gen/gen_contents.go:330 - form parameter "tags": github.com/ogen-go/ogen/gen.(*Generator).generateFormContent /Users/ritza/go/pkg/mod/github.com/ogen-go/ogen@v0.69.1/gen/gen_contents.go:206 - nested objects not allowed: github.com/ogen-go/ogen/gen.isParamAllowed /Users/ritza/go/pkg/mod/github.com/ogen-go/ogen@v0.69.1/gen/gen_parameters.go:184 exit status 1 generate.go:3: running "go": exit status 1 ``` The function `isParamAllowed` in `gen/gen_parameters.go` on line 184 throws the error that nested objects are not allowed in form parameters. This seems to indicate that ogen does not yet support generating an SDK for a form request that takes nested objects as parameters, such as a pet's tags, when updating a pet in our schema. The ogen documentation references a spec to download, so we'll replace `petstore.yaml` with their spec by running the following: ```bash curl https://raw.githubusercontent.com/ogen-go/web/main/examples/petstore.yml --output petstore.yaml ``` With this new simplified spec, we'll try the generator again: ```bash go generate ./... ``` The generator runs without errors and prints a log line: ``` INFO convenient Convenient errors are not available {"reason": "operation has no \"default\" response", "at": "petstore.yaml:19:9"} ``` This log line seems to indicate that some operations don't return a default response. ### Speakeasy generate Finally, we'll generate an SDK using the Speakeasy CLI. ```bash # Generate Petstore SDK using Speakeasy go generator speakeasy quickstart ``` This command prints a log of warnings and information, then completes successfully. ## SDK code compared: Package structure We now have four different Go SDKs for the Petstore API: - `./petstore-sdk-go-openapi/`, generated by OpenAPI Generator. - `./petstore-sdk-go-oapi-codegen/`, generated by oapi-codegen. - `./petstore-sdk-go-ogen/`, generated by ogen. - `./petstore-sdk-go-speakeasy/`, generated by Speakeasy. We'll start our comparison by looking at the structure of each generated SDK. Let's print a tree structure for each SDK's directory. Run `tree` in the terminal from our root directory: ```bash tree -L 3 -F petstore-sdk-go-* ``` We'll split the output by directory for each SDK below. ### OpenAPI Generator SDK structure ```bash petstore-sdk-go-openapi/ ├── README.md ├── api/ │ └── openapi.yaml ├── api_pet.go ├── api_store.go ├── api_user.go ├── client.go ├── configuration.go ├── docs/ │ ├── Address.md │ ├── APIResponse.md │ ├── Category.md │ ├── Customer.md │ ├── Order.md │ ├── Pet.md │ ├── PetApi.md │ ├── StoreApi.md │ ├── Tag.md │ ├── User.md │ └── UserApi.md ├── git_push.sh ├── go.mod ├── go.sum ├── model_address.go ├── model_api_response.go ├── model_category.go ├── model_customer.go ├── model_order.go ├── model_pet.go ├── model_tag.go ├── model_user.go ├── response.go ├── test/ │ ├── api_pet_test.go │ ├── api_store_test.go │ └── api_user_test.go └── utils.go OpenAPI Generator creates a relatively flat directory structure, with dedicated directories only for a copy of the spec (`api/openapi.yaml`), documentation (`docs/`), and tests (`test/`). ### oapi-codegen SDK structure ```bash petstore-sdk-go-oapi-codegen/ ├── go.mod └── petstore.gen.go ``` oapi-codegen creates only one file for all generated code, with no tests or documentation outside this file. This generator appears to be better suited to generating a small and specific client or server as part of a larger project, rather than generating a usable SDK that can be packaged for users. ### ogen SDK structure ```bash petstore-sdk-go-ogen/ ├── generate.go ├── go.mod ├── petstore/ │ ├── oas_cfg_gen.go │ ├── oas_client_gen.go │ ├── oas_interfaces_gen.go │ ├── oas_json_gen.go │ ├── oas_parameters_gen.go │ ├── oas_request_encoders_gen.go │ ├── oas_response_decoders_gen.go │ ├── oas_schemas_gen.go │ └── oas_validators_gen.go └── petstore.yaml ``` ogen also generates relatively few files, which does not seem to be because this generation was based on a simpler spec. This generator does not seem to split schemas into different files and does not create any tests or documentation. ### Speakeasy SDK structure ```bash petstore-sdk-go-speakeasy/ ├── README.md* ├── USAGE.md* ├── docs/ │ ├── models/ │ │ ├── operations/ │ │ └── shared/ │ └── sdks/ │ ├── pet/ │ ├── sdk/ │ ├── store/ │ └── user/ ├── files.gen* ├── gen.yaml* ├── go.mod* ├── go.sum* ├── pet.go* ├── pkg/ │ ├── models/ │ │ ├── operations/ │ │ └── shared/ │ ├── types/ │ │ ├── bigint.go* │ │ ├── date.go* │ │ └── datetime.go* │ └── utils/ │ ├── contenttype.go* │ ├── form.go* │ ├── headers.go* │ ├── pathparams.go* │ ├── queryparams.go* │ ├── requestbody.go* │ ├── retries.go* │ ├── security.go* │ └── utils.go* ├── sdk.go* ├── store.go* └── user.go* ``` Speakeasy generates a clear file structure, split into directories for models, types, and other utils. It also creates documentation, split by models and endpoints. ## Models Let's compare how a pet is represented in each of the four SDKs: ### OpenAPI Generator pet model ```go // OpenAPI Generator pet model type Pet struct { Id *int64 `json:"id,omitempty"` Name string `json:"name"` Category *Category `json:"category,omitempty"` PhotoUrls []string `json:"photoUrls"` Tags []Tag `json:"tags,omitempty"` // pet status in the store Status *string `json:"status,omitempty"` } ``` OpenAPI Generator does not seem to take the spec's enum for pet status when generating the pet model. Status in this model is a pointer to a string, while other generators create a type to validate the status field. This model includes struct tags for JSON only. ### oapi-codegen pet model ```go // oapi-codegen pet model type Pet struct { Category *Category `json:"category,omitempty"` Id *int64 `json:"id,omitempty"` Name string `json:"name"` PhotoUrls []string `json:"photoUrls"` Status *PetStatus `json:"status,omitempty"` Tags *[]Tag `json:"tags,omitempty"` } ``` The oapi-codegen pet model is similar to the OpenAPI Generator version, but it makes the `Tags` field a pointer to a slice of `Tag`, making it possible for this field to be `nil` (which would be omitted from the JSON due to `omitempty`). The `Status` field is not a simple string pointer, but a pointer to `PetStatus`, which provides better type safety, since `PetStatus` is a type alias for `string` with specific allowable values. ### ogen pet model ```go // ogen pet model type Pet struct { ID OptInt64 `json:"id"` Name string `json:"name"` PhotoUrls []string `json:"photoUrls"` Status OptPetStatus `json:"status"` } ``` The pet model generated by ogen lacks the `Tags` and `Category` fields because these fields are not present in the simplified spec used. This struct uses a different approach to optional fields. It uses `OptInt64` and `OptPetStatus` types. We'll look at how ogen differs from Speakeasy in terms of nullable fields below. ### Speakeasy pet model ```go // Speakeasy pet model type Pet struct { Category *Category `json:"category,omitempty" form:"name=category,json"` ID *int64 `json:"id,omitempty" form:"name=id"` Name string `json:"name" form:"name=name"` PhotoUrls []string `json:"photoUrls" form:"name=photoUrls"` // pet status in the store Status *PetStatus `json:"status,omitempty" form:"name=status"` Tags []Tag `json:"tags,omitempty" form:"name=tags,json"` } ``` This struct is similar to the OpenAPI Generator version but includes additional `form` struct tags, which are likely used to specify how these fields should be encoded and decoded when used in form data (such as in an HTTP POST request). Like the oapi-codegen version, `Status` is a `*PetStatus` rather than a `*string`. ## Nullable fields Let's focus on the difference between how ogen and Speakeasy handle the nullable `Status` field. Here's the relevant code generated by ogen: ```go type PetStatus string const ( PetStatusAvailable PetStatus = "available" PetStatusPending PetStatus = "pending" PetStatusSold PetStatus = "sold" ) // OptPetStatus is optional PetStatus. type OptPetStatus struct { Value PetStatus Set bool } ``` While much safer than the OpenAPI Generator's pointer to a string type, the ogen `OptPetStatus` is not idiomatic and provides no benefit over using pointers, as Speakeasy does: ```go type PetStatus string const ( PetStatusAvailable PetStatus = "available" PetStatusPending PetStatus = "pending" PetStatusSold PetStatus = "sold" ) func (e PetStatus) ToPointer() *PetStatus { return &e } ``` The Speakeasy approach provides the same strong typing as the ogen version. It defines `PetStatus` as a custom string type and defines allowable values as constants. This practice ensures that you can't accidentally set a `PetStatus` to an invalid value. The way Speakeasy handles the `PetStatus` type is more idiomatic to Go, which generally favors simplicity and readability. Instead of defining a new struct like `OptPetStatus`, Speakeasy uses a built-in language feature (pointers) to achieve the same effect. This approach is simpler, more consistent with the rest of the language, and easier to understand and use. ## SDK dependencies The ogen and oapi-codegen SDKs don't add any dependencies to the generated modules, so we'll compare dependencies between OpenAPI Generator and Speakeasy SDKs. We'll run the following for each of these two SDKs: ```bash go mod graph ``` For Speakeasy, this command prints the following: ``` openapi github.com/cenkalti/backoff/v4@v4.2.0 openapi github.com/spyzhov/ajson@v0.8.0 ``` The output for the OpenAPI Generator version is too long to show here, so we'll do a count instead: ```bash go mod graph | wc -l #> 1538 ``` Speakeasy purposefully generates SDKs with fewer dependencies, which leads to faster installs, reduced build times, and less exposure to potential security vulnerabilities. To see why the Speakeasy SDK depends on an exponential backoff module, let's discuss retries. ## Retries The SDK generated by Speakeasy can automatically retry failed network requests or retry requests based on specific error responses, providing a straightforward developer experience for error handling. To enable this feature, we need to use the Speakeasy `x-speakeasy-retries` extension to the OpenAPI spec. We'll update the OpenAPI spec to add retries to the `addPet` operation as a test. Edit `petstore.yaml` and add the following to the `addPet` operation: ```yaml x-speakeasy-retries: strategy: backoff backoff: initialInterval: 500 # 500 milliseconds maxInterval: 60000 # 60 seconds maxElapsedTime: 3600000 # 5 minutes exponent: 1.5 ``` Add this snippet to the operation: ```yaml #... paths: /pet: # ... post: #... operationId: addPet x-speakeasy-retries: strategy: backoff backoff: initialInterval: 500 # 500 milliseconds maxInterval: 60000 # 60 seconds maxElapsedTime: 3600000 # 5 minutes exponent: 1.5 ``` Now we'll rerun the Speakeasy generator to enable retries for failed network requests when creating a new pet. It is also possible to enable retries for the SDK as a whole by adding global `x-speakeasy-retries` at the root of the OpenAPI spec. ## Generated documentation Both Speakeasy and OpenAPI generate documentation for the generated code. Each generator creates a `README.md` file at the base directory of the generated SDK. This file serves as a primary source of documentation for the SDK users. You have the option to [customize this README](/docs/sdk-docs/edit-readme) using Speakeasy to suit your needs better. For example, you could add your brand's logo, provide links for support, outline a code of conduct, or include any other information that could be useful to the developers using the SDK. The Speakeasy-generated documentation really shines when it comes to usage examples, which include working usage examples for all operations, complete with imports and appropriately formatted string examples. For instance, if a string is formatted as `email` in our OpenAPI spec, Speakeasy generates usage examples with strings that look like email addresses. Types formatted as `uri` will generate examples that look like URLs. This makes example code clear and scannable. We'll test this by adding `format: uri` to the items in the `photoUrls` array. Let's compare the generated example code for the `addPet` endpoint after adding this string format. ### Usage example generated by OpenAPI Here's the example from the OpenAPI-generated documentation: ```go package main import ( "context" "fmt" "os" openapiclient "github.com/GIT_USER_ID/GIT_REPO_ID" ) func main() { pet := *openapiclient.NewPet("doggie", []string{"PhotoUrls_example"}) // Pet | Create a new pet in the store configuration := openapiclient.NewConfiguration() apiClient := openapiclient.NewAPIClient(configuration) resp, r, err := apiClient.PetApi.AddPet(context.Background()).Pet(pet).Execute() if err != nil { fmt.Fprintf(os.Stderr, "Error when calling `PetApi.AddPet``: %v\n", err) fmt.Fprintf(os.Stderr, "Full HTTP response: %v\n", r) } // response from `AddPet`: Pet fmt.Fprintf(os.Stdout, "Response from `PetApi.AddPet`: %v\n", resp) } ``` Note how the OpenAPI example only includes required fields and ignores the URI string format from our spec. ### Usage example generated by Speakeasy This is what Speakeasy generates as a usage example: ```go package main import( "context" "log" "openapi" "openapi/pkg/models/shared" "openapi/pkg/models/operations" ) func main() { s := sdk.New() ctx := context.Background() res, err := s.Pet.AddPetJSON(ctx, shared.Pet{ Category: &shared.Category{ ID: sdk.Int64(1), Name: sdk.String("Dogs"), }, ID: sdk.Int64(10), Name: "doggie", PhotoUrls: []string{ "https://ecstatic-original.info", }, Status: shared.PetStatusSold.ToPointer(), Tags: []shared.Tag{ shared.Tag{ ID: sdk.Int64(681820), Name: sdk.String("Stacy Moore"), }, shared.Tag{ ID: sdk.Int64(697631), Name: sdk.String("Brenda Wisozk"), }, shared.Tag{ ID: sdk.Int64(670638), Name: sdk.String("Connie Herzog"), }, shared.Tag{ ID: sdk.Int64(315428), Name: sdk.String("Corey Hane III"), }, }, }, operations.AddPetJSONSecurity{ PetstoreAuth: "", }) if err != nil { log.Fatal(err) } if res.Pet != nil { // handle response } } ``` The example generated by Speakeasy includes all available fields and correctly formats the example string in the `PhotoUrls` field. We'll also compare how OpenAPI and Speakeasy generate documentation for the `Status` field in our pet model. ### OpenAPI generate does not document enums The OpenAPI-generated documentation reflects the generated code's omission of valid options for the `Status` field. Here's the pet model documentation generated by OpenAPI: #### Pet properties generated by OpenAPI
### Speakeasy generates documentation showing valid values Here's how Speakeasy documents the pet model: #### Pet fields generated by Speakeasy
In the example above, `PetStatus` links to the following documentation: #### PetStatus values generated by Speakeasy
## Automation This comparison focuses on the installation and usage of command line generators, but the Speakeasy generator can also run as part of a CI workflow, for instance as a [GitHub Action](https://github.com/speakeasy-api/sdk-generation-action), to make sure your SDK is always up to date when your API spec changes. ## Summary We've seen how Speakeasy generates lightweight, idiomatic SDKs for Go. If you're building an API that developers rely on and would like to publish full-featured Go SDKs that follow best practices, we strongly recommend giving the [Speakeasy SDK generator](/docs/speakeasy-reference/cli/getting-started) a try. [Join our Slack community](https://go.speakeasy.com/slack) to let us know how we can improve our Go SDK generator or to suggest features. # Java Feature Reference Source: https://speakeasy.com/docs/sdks/languages/java/feature-support import { Table } from "@/mdx/components"; ## Authentication
## Server Configuration
## Data Types ### Basic Types
### Polymorphism
## Methods
## Parameters
### Path Parameters Serialization
### Query Parameters Serialization
## Requests
## Responses
## Documentation
# Generate a Java SDK from OpenAPI / Swagger Source: https://speakeasy.com/docs/sdks/languages/java/methodology-java import { FileTree } from "nextra/components"; ## Overview Speakeasy generates Java SDKs that integrate naturally with existing Java ecosystems, following established conventions for consistency with hand-written libraries. The generated code provides full IDE support, compile-time validation, and seamless integration with standard tooling. **Design principles:** - **Native Java ergonomics**: Code generation leverages Java's type system, generics, and method chaining to create APIs that feel natural to Java developers. Builder patterns, fluent interfaces, and standard library types eliminate the need to learn framework-specific abstractions - **Comprehensive type safety**: Strong typing catches API contract violations at compile time, while JSR-305/Jakarta nullability annotations provide rich IDE warnings and autocomplete derived directly from your OpenAPI specification - **Flexible concurrency models**: Synchronous execution by default supports traditional blocking patterns, while `.async()` mode provides `CompletableFuture` and reactive streams support for non-blocking architectures—enabling incremental adoption without rewriting existing code - **Minimal runtime dependencies**: Built on Java standard library primitives like `CompletableFuture` and `Flow.Publisher` rather than heavyweight frameworks, ensuring clean integration into existing codebases and microservice architectures - **Built-in observability**: SLF4J integration provides structured logging across all SDK operations without framework lock-in, enabling comprehensive monitoring of HTTP requests, retries, streaming, and hook execution - **Specification fidelity**: Method signatures, documentation, and validation rules generated directly from OpenAPI definitions maintain accuracy between API contracts and client code, reducing integration surprises ```java // Initialize SDK with builder pattern - idiomatic Java design SDK sdk = SDK.builder() .serverURL("https://api.example.com") .apiKey("your-api-key") .build(); // Type-safe method chaining with IDE autocomplete User user = sdk.users() .userId("user-123") // Required field - compile-time safety .includeMetadata(true) // Optional field - null-friendly .call(); // Synchronous by default // Seamless async with same API - just add .async() CompletableFuture asyncUser = sdk.async().users() .userId("user-123") .includeMetadata(true) .call(); // Native reactive streams support Publisher orderStream = sdk.async().orders() .status("active") .callAsPublisher(); // Pagination with familiar Java patterns sdk.orders() .status("completed") .callAsStream() // Returns java.util.Stream .filter(order -> order.amount() > 100) .limit(50) .forEach(System.out::println); // Rich exception handling with context try { User result = sdk.users().userId("invalid").call(); } catch (APIException e) { // Detailed error context from OpenAPI spec System.err.println("API Error: " + e.getMessage()); System.err.println("Status: " + e.statusCode()); } ``` ## Core Features ### Type Safety & Null Handling The SDK provides compile-time validation and runtime checks for required fields, with intuitive null handling: - **Compile-time validation**: Strong typing catches problems before runtime - **Runtime validation**: Required fields throw exceptions if missing - **Null-friendly setters**: Simple setters without Optional/JsonNullable wrapping - **Smart getters**: Return types match field semantics - direct access for required fields, `Optional` for non-required fields, and [`JsonNullable`](https://github.com/OpenAPITools/jackson-databind-nullable) for non-required nullable fields ```java // Builder with various field types User user = User.builder() .id(123L) // Required primitive .name("John Doe") // Required string .email("john@example.com") // Required field .age(30) // Optional primitive - defaults if not set .bio("Developer") // Optional string - can be null .profileImage(null) // Nullable field - accepts null explicitly .build(); // Throws runtime exception if required fields missing // Type-safe getters with semantically appropriate return types String name = user.name(); // Direct access for required fields Optional age = user.age(); // Optional for non-required fields JsonNullable bio = user.bio(); // JsonNullable for non-required + nullable fields // Method chaining with runtime validation CreateUserRequest request = CreateUserRequest.builder() .user(user) // Required - runtime exception if missing .sendWelcomeEmail(true) // Optional boolean .metadata(Map.of("source", "api")) // Optional complex type .build(); // Validates all required fields ``` ### Fluent Call-Builder Chaining The SDK supports fluent method chaining that combines method builders with request builders for intuitive API calls: ```java // Fluent chaining: method builder → parameters → request body → call User res = sdk.updateUser() .id("user-123") // Path/query parameters .payload(PatchUser.builder() .name("John Doe") // Request body fields .email("john@example.com") .build()) .call(); // Execute request ``` ### Authentication & Security - OAuth flows and standard security mechanisms - Custom enum types using string or integer values - All-field constructors for compile-time OpenAPI change detection ## Synchronous Methods ### Basic Methods Synchronous methods are the default mode for all SDK calls: ```java // Standard synchronous calls Portfolio portfolio = sdk.getPortfolio("user-123"); List trades = sdk.getTrades(portfolio.getId()); ``` ### Pagination For synchronous pagination, use `.call()`, `.callAsIterable()`, `.callAsStream()`, or `.callAsStreamUnwrapped()`: - **`.call()`**: Returns the first page only - **`.callAsIterable()`**: Returns `Iterable<>` for for-each iteration with automatic paging - **`.callAsStream()`**: Returns `java.util.Stream` of pages with automatic paging - **`.callAsStreamUnwrapped()`**: Returns `java.util.Stream` of concatenated items from all pages ```java // Stream unwrapped example sdk.searchDocuments() .contains("simple") .minSize(200) .maxSize(400) .callAsStreamUnwrapped() // Returns Stream .filter(document -> "fiction".equals(document.category())) .limit(200) .forEach(System.out::println); ``` ### Server-Sent Events For synchronous SSE, use the `events()` method with try-with-resources: ```java // Traverse event stream with while loop try (EventStream events = response.events()) { Optional event; while ((event = events.next()).isPresent()) { processEvent(event.get()); } } // Use with java.util.Stream try (EventStream events = response.events()) { events.stream().forEach(this::processEvent); } ``` ### Error Handling The SDK throws typed unchecked exceptions for all errors, organized in a hierarchy: ``` Base SDK Error ├── Default SDK Error (for network/IO errors and untyped API errors) ├── Default Async SDK Error (for async-specific errors) └── Custom Errors (for typed error responses defined in OpenAPI spec) ``` Error class names can be customized via `gen.yaml` flags; if not specified, they're inferred from the SDK name. #### Base SDK Error All exceptions extend `RuntimeException` and encapsulate the raw HTTP response with accessors for: - **Status code**: `statusCode()` - **Headers**: `headers()` - **Body**: `body()` returns `Optional` and `bodyAsString()` returns `Optional` (accounts for cases where the body couldn't be read due to `IOException`) #### Default SDK Error The default SDK error is thrown during: - **Network/IO errors**: Connection failures, timeouts, and other transport-level issues - **Untyped API errors**: HTTP error responses without custom error schemas defined in the OpenAPI spec #### Custom Error Responses For operations with error responses defined in the OpenAPI spec, the SDK generates typed exception classes that encapsulate the error schema. **Example OpenAPI spec:** ```yaml paths: /users/{userId}: get: responses: '200': description: OK content: application/json: schema: $ref: '#/components/schemas/User' '404': description: Not Found content: application/json: schema: $ref: '#/components/schemas/UserError' components: schemas: UserError: type: object properties: code: type: string enum: [NotFound, Unauthorized] reason: type: string ``` **Generated exception class:** ```java public class UserError extends BaseSdkError { // Error schema as nested static class public static class Data { // Generated enum from OpenAPI spec public enum Code { NOT_FOUND("NotFound"), UNAUTHORIZED("Unauthorized"); private final String value; Code(String value) { this.value = value; } public String value() { return value; } } private Code code; private String reason; // Getters and setters... } // Hoisted field accessors for convenience public Optional code() { ... } public Optional reason() { ... } // Full error object accessor public Optional data() { ... } // Available if deserialization failed public Optional deserializationError() { ... } } ``` All accessors return `Optional` to handle cases where the response body couldn't be deserialized. **Usage:** ```java try { User user = sdk.getUser("user-123"); } catch (UserError e) { // Handle typed error with field access e.code().ifPresent(code -> System.err.println("Error Code: " + code)); e.reason().ifPresent(reason -> System.err.println("Reason: " + reason)); // Check for deserialization issues if (e.deserializationError().isPresent()) { System.err.println("Failed to parse error response"); } } catch (SDKError e) { // Handle default SDK errors (network issues, untyped errors) System.err.println("Request failed: " + e.getMessage()); System.err.println("Status: " + e.statusCode()); } ``` ## Asynchronous Methods ### Dual SDK Architecture Speakeasy Java SDKs implement a dual interface pattern that provides both synchronous and asynchronous programming models without breaking changes: - **Synchronous by default**: All SDK instances work synchronously out of the box, maintaining compatibility with existing code. - **Async opt-in**: Call `.async()` on any SDK instance to switch to asynchronous mode for that method chain. - **Consistent API**: The same methods and parameters work in both modes, only the return types differ. ```java // Single SDK instance serves both paradigms TradingSDK sdk = TradingSDK.builder() .serverURL("https://api.example.com") .apiKey("your-api-key") .build(); // Synchronous usage Portfolio portfolio = sdk.getPortfolio("user-123"); List trades = sdk.getTrades(portfolio.getId()); // Asynchronous usage CompletableFuture asyncPortfolio = sdk.async() .getPortfolio("user-123"); CompletableFuture> asyncTrades = asyncPortfolio .thenCompose(p -> sdk.async().getTrades(p.getId())); ``` ### Non-blocking I/O implementation The async implementation uses Java 11's `HttpClient` async APIs and NIO.2 primitives for end-to-end non-blocking method calls: - **HTTP requests**: `HttpClient.sendAsync()` with `CompletableFuture>` - **File I/O**: `AsynchronousFileChannel` for non-blocking file methods - **Stream processing**: `Flow.Publisher>` for efficient byte handling ```java // Underlying HTTP implementation client.sendAsync(request, HttpResponse.BodyHandlers.ofPublisher()) .thenApply(response -> response.body()) // Flow.Publisher> .thenCompose(this::decodeJsonAsync); ``` ### Reactive Streams integration For async iterables, the SDK leverages [Reactive Streams](https://www.reactive-streams.org/) `Publisher` to provide: - **Backpressure handling**: Consumers control the rate of data processing - **Ecosystem interoperability**: Works with [Project Reactor](https://projectreactor.io/), RxJava, Akka Streams, and other reactive libraries - **Resource efficiency**: Memory-efficient processing of large datasets - **Composition**: Chain and transform async streams declaratively The examples in this documentation use `Flux` from [Project Reactor](https://projectreactor.io/) to demonstrate interoperability with reactive frameworks. The SDK implements custom publishers, subscribers, and subscriptions using JDK-native operators while maintaining lightweight dependencies. ### Async Pagination For async pagination, use `callAsPublisher()` and `callAsPublisherUnwrapped()` methods that return reactive streams: ```java // Async pagination - returns Publisher Publisher userPages = sdk.async().listUsers() .callAsPublisher(); // Async pagination unwrapped - returns Publisher (concatenated items) Publisher users = sdk.async().listUsers() .callAsPublisherUnwrapped(); // Use with reactive libraries (Flux is from Project Reactor) Flux.from(users) .filter(User::isActive) .take(100) .subscribe(this::processUser); ``` ### Async Server-Sent Events For async SSE, `EventStream` implements `Publisher` directly: ```java // Async SSE streaming - EventStream implements Publisher and handles async response EventStream eventStream = sdk.async().streamLogs().events(); // Process with reactive libraries - EventStream is a Publisher Flux.from(eventStream) .filter(event -> "ERROR".equals(event.getLevel())) .subscribe(this::handleErrorEvent); ``` ### Migration & DevX Improvements Async-enabled SDKs provide backward compatibility, gradual adoption via `.async()` calls, and compatibility with Java 21+ virtual threads. Additional enhancements include null-friendly parameters, Jakarta annotations, enhanced error handling, and built-in timeout/cancellation support. ### Package Structure ## Advanced Topics ### Blob Abstraction The `Blob` class provides efficient byte-stream handling across both sync and async methods: ```java // Create from various sources Blob.from(Paths.get("large-file.json")); // File path Blob.from(inputStream); // InputStream Blob.from("content"); // String Blob.from(publisherOfByteBuffers); // Flow.Publisher // Async consumption CompletableFuture bytes = blob.toByteArray(); CompletableFuture path = blob.toFile(targetPath); Publisher stream = blob.asPublisher(); ``` ### HTTP Client Customization The Java SDK HTTP client is configurable and supports both synchronous and asynchronous methods: ```java public interface HTTPClient { HttpResponse send(HttpRequest request) throws IOException, InterruptedException, URISyntaxException; CompletableFuture> sendAsync(HttpRequest request); } ``` A default implementation based on `java.net.HttpClient` provides sync and async patterns with connection pooling and streaming support. ### Custom Headers Custom headers (those not explicitly defined in the OpenAPI spec) can be specified per-request using the call builder: ```java CreatePaymentResponse res = sdk.payments().create() .paymentRequest(PaymentRequest.builder() .description("My first payment") .redirectUrl("https://example.org/redirect") .amount(10) .build()) .header("IdempotencyKey", nextKey()) // custom header .call(); ``` ### Data Types & Serialization The SDK uses native Java types where possible and provides custom handling for complex OpenAPI constructs. #### Primitive and Native Types Where possible, the Java SDK uses native types and primitives to increase null safety: - `java.lang.String` - `java.time.OffsetDateTime` for `date-time` format - `java.time.LocalDate` for `date` format - `java.math.BigInteger` for unlimited-precision integers - `java.math.BigDecimal` for unlimited-precision decimals - `int` (or `java.lang.Integer`) - `long` (or `java.lang.Long`) - `float` (or `java.lang.Float`) - `double` (or `java.lang.Double`) - `boolean` (or `java.lang.Boolean`) #### Unlimited-Precision Numerics For applications requiring high-precision decimal or integer types (such as monetary amounts), use format specifications: ```yaml # Unlimited-precision integer type: integer format: bigint # OR type: string format: bigint ``` ```yaml # Unlimited-precision decimal type: number format: decimal # OR type: string format: decimal ``` Both map to `java.math.BigInteger` and `java.math.BigDecimal` respectively, with convenient builder overloads: ```java // Object builders accept primitives directly Payment.builder() .amount(99.99) // Accepts double, converts to BigDecimal .transactionId(12345L) // Accepts long, converts to BigInteger .build(); ``` #### Union Types Support for polymorphic types uses OpenAPI's `oneOf` keyword with different strategies based on discriminator presence. **Non-discriminated oneOf** uses composition: ```yaml Pet: oneOf: - $ref: "#/components/schemas/Cat" - $ref: "#/components/schemas/Dog" ``` ```java Cat cat = Cat.builder().name("Whiskers").build(); Dog dog = Dog.builder().name("Rex").build(); // Pet.of accepts only Cat or Dog types Pet pet = Pet.of(cat); // Type inspection for handling if (pet.value() instanceof Cat cat) { System.out.println("Cat: " + cat.name()); } else if (pet.value() instanceof Dog dog) { System.out.println("Dog: " + dog.name()); } ``` **Discriminated oneOf** uses inheritance: ```yaml Pet: oneOf: - $ref: "#/components/schemas/Cat" - $ref: "#/components/schemas/Dog" discriminator: propertyName: petType ``` ```java Pet cat = Cat.builder().name("Whiskers").build(); // Cat implements Pet Pet dog = Dog.builder().name("Rex").build(); // Dog implements Pet ``` **oneOf Type Erasure Handling:** When generic types would conflict, suffixed factory methods are generated: ```yaml Info: oneOf: - type: array items: { type: integer } x-speakeasy-name-override: counts - type: array items: { type: string } x-speakeasy-name-override: descriptions ``` ```java // Generates specialized factory methods to avoid erasure Info countsInfo = Info.ofCounts(List.of(1, 2, 3)); Info descriptionsInfo = Info.ofDescriptions(List.of("a", "b", "c")); ``` **anyOf Support:** The `anyOf` keyword is treated as `oneOf` with forgiving deserialization—when multiple subtypes match, the heuristic selects the subtype with the greatest number of matching properties. #### Enums **Closed Enums** (standard Java enum): ```java public enum Color { RED("red"), GREEN("green"), BLUE("blue"); @JsonValue private final String value; public String value() { return value; } public static Optional fromValue(String value) { // Returns Optional.empty() for unknown values } } ``` **Open Enums** with `x-speakeasy-unknown-values: allow`: ```yaml Color: type: string enum: [red, green, blue] x-speakeasy-unknown-values: allow ``` Generates a concrete class instead of Java enum: ```java // Looks like enum but handles unknown values Color red = Color.RED; // Static constants Color unknown = Color.of("purple"); // Handles unknown values boolean isUnknown = unknown.isUnknown(); // Check if value is unknown // For switch expressions, convert to real enum unknown.asEnum().ifPresent(knownColor -> { switch (knownColor) { case RED -> System.out.println("Red"); // ... handle known values } }); ``` #### Custom Serialization You **must** use the generated custom Jackson `ObjectMapper` for serialization/deserialization: ```java // Access the singleton ObjectMapper ObjectMapper mapper = JSON.getMapper(); // Serialize/deserialize generated objects String json = mapper.writeValueAsString(user); User user = mapper.readValue(json, User.class); ``` ### Build Customization - **Preserve customizations**: Use `build-extras.gradle` for additions (untouched by generation updates) - **Add plugins**: Use `additionalPlugins` property in `gen.yaml` - **Manage dependencies**: Add to `build-extras.gradle` or use `additionalDependencies` in `gen.yaml` ```yaml java: version: 0.2.0 --- additionalPlugins: - 'id("java")' additionalDependencies: - implementation:com.fasterxml.jackson.core:jackson-databind:2.16.0 ``` ### Logging & Observability #### SLF4J Integration The Java SDK includes built-in SLF4J logging integration that provides structured logging across all SDK operations without requiring specific logging implementations. This backend-agnostic approach allows library authors to include logging without depending on particular logging frameworks. **Configuration:** SLF4J logging is enabled by default for new SDKs via the `enableSlf4jLogging` configuration option. The feature can be configured in `gen.yaml`: ```yaml java: version: 1.0.0 enableSlf4jLogging: true # Default: true for new SDKs ``` **Log Levels:** - **DEBUG**: High-level operations (request/response cycles, retry attempts, hook executions) - **TRACE**: Detailed information (pagination state, streaming lifecycle, backoff calculations) **Logging Coverage:** - **HTTP Client**: Request/response logging with sensitive header redaction - **Retry Logic**: Attempt tracking, backoff calculations, and exhaustion reporting - **Pagination**: Page fetch tracking and state management - **Streaming**: Initialization, item processing, and lifecycle events - **Hooks**: Execution counts, operation IDs, and exception handling **Setup Example:** Add a logging implementation dependency to your project: ```gradle dependencies { implementation 'org.slf4j:slf4j-api:2.0.9' implementation 'ch.qos.logback:logback-classic:1.5.6' // Example implementation } ``` Create a `logback.xml` configuration file: ```xml %d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n ``` **Backward Compatibility:** Legacy `enableHTTPDebugLogging()` continues to work for existing SDKs. Both logging approaches can coexist during migration periods. **Security:** Sensitive headers (Authorization, X-API-Key, etc.) are automatically redacted in HTTP request/response logs to prevent credential leakage. ### Dependencies The SDK maintains minimal dependencies: - [Jackson Library](https://github.com/FasterXML/jackson) for JSON serialization/deserialization - [Apache HttpClient](https://hc.apache.org/httpcomponents-client-4.5.x/index.html) for HTTP requests - [Jayway JsonPath](https://github.com/json-path/JsonPath) for JSON path expressions in Speakeasy metadata - [SLF4J API](https://www.slf4j.org/) for structured logging (when enabled) ## Configuration Reference ### Parameters & Method Generation Method parameter handling is controlled by the `maxMethodParams` configuration in `gen.yaml`: ```yaml java: version: 1.0.0 maxMethodParams: 5 # Default threshold ``` **When parameter count ≤ `maxMethodParams`:** ```java // Parameters become method arguments User user = sdk.getUser("user-123", true, "email,name"); ``` **When parameter count > `maxMethodParams` or `maxMethodParams = 0`:** ```java // All parameters wrapped in request object GetUserRequest request = GetUserRequest.builder() .userId("user-123") .includeMetadata(true) .fields("email,name") .build(); User user = sdk.getUser(request); ``` ### Default Values & Constants The SDK handles OpenAPI `default` and `const` keywords with lazy-loading behavior: #### Default Values Fields with `default` values use `Optional` wrappers and lazy-loading: ```yaml # OpenAPI specification User: type: object properties: status: type: string default: "active" ``` ```java // Usage - passing Optional.empty() uses the OpenAPI default User user = User.builder() .name("John") .status(Optional.empty()) // Will use "active" from OpenAPI spec .build(); // Or omit the field entirely in builders User user = User.builder() .name("John") // status not specified - uses OpenAPI default "active" .build(); ``` **Important:** Default values are lazy-loaded once. If the OpenAPI default is invalid for the field type (e.g., `default: abc` for `type: integer`), an `IllegalArgumentException` is thrown. **Workarounds for invalid defaults:** 1. Regenerate SDK with corrected OpenAPI default 2. Always set the field explicitly to avoid lazy-loading the invalid default #### Constant Values Fields with `const` values are read-only and set internally: ```yaml # OpenAPI specification ApiResponse: type: object properties: version: type: string const: "1.0" ``` ```java // Const fields are not settable in constructors or builders ApiResponse response = ApiResponse.builder() .data(responseData) // version is automatically set to "1.0" - cannot be overridden .build(); // But const values are readable via getters String version = response.version(); // Returns "1.0" ``` Like default values, `const` values are lazy-loaded once. Invalid const values throw `IllegalArgumentException`. ### User Agent Strings The Java SDK includes a user agent string in all requests for tracking SDK usage: ``` speakeasy-sdk/java {{SDKVersion}} {{GenVersion}} {{DocVersion}} {{groupId.artifactId}} ``` - `SDKVersion`: SDK version defined in `gen.yaml` - `GenVersion`: Speakeasy generator version - `DocVersion`: OpenAPI document version - `groupId.artifactId`: Concatenated from `gen.yaml` configuration # Comparison guide: OpenAPI/Swagger Java client generation Source: https://speakeasy.com/docs/sdks/languages/java/oss-comparison-java import { Table } from "@/mdx/components"; At Speakeasy, the team specializes in producing idiomatic SDKs in various programming languages, including Java. The approach to SDK generation prioritizes a rich developer experience that enables API providers to concentrate on refining APIs and empowers developer-users to efficiently leverage services. In this article, we'll compare creating a Java SDK using Speakeasy to creating one using the [OpenAPI Generator](https://github.com/OpenAPITools/openapi-generator). Below is a table that summarizes the key differences between the two SDKs:
Read more about these headline features of Speakeasy-created Java SDKs in the [March 2024 release notes](/post/release-java-ga), or consult the [Speakeasy Java SDK documentation](/docs/languages/java/methodology-java). For a detailed technical comparison, read on! ## Installing the CLIs For this comparison, we need both the Speakeasy and OpenAPI Generator CLIs installed to generate the Java SDKs from the specification YAML file. We're using macOS, so we use Homebrew to install the CLIs. ### Installing the Speakeasy CLI Install the Speakeasy CLI by running the following command in the terminal: ```bash brew install speakeasy-api/tap/speakeasy ``` You can check the version to ensure installation was successful: ```bash speakeasy -v ``` If you encounter any errors, take a look at the [Speakeasy SDK creation documentation](/docs/sdks/create-client-sdks). ### Installing the OpenAPI Generator CLI Install the OpenAPI Generator CLI by running the following command in the terminal: ```bash brew install openapi-generator ``` You can check the version: ```bash openapi-generator version ``` Browse the [OpenAPI Generator documentation](https://github.com/OpenAPITools/openapi-generator/blob/master/README.md) if any errors occur. ## Downloading the Swagger Petstore specification We need an OpenAPI specification YAML file to generate SDKs for. We'll use the Swagger Petstore specification, which you can find at [https://petstore3.swagger.io/api/v3/openapi.yaml](https://petstore3.swagger.io/api/v3/openapi.yaml). Download the file in and save it as `petstore.yaml` with the following command in the terminal: ```bash curl https://petstore3.swagger.io/api/v3/openapi.yaml --output petstore.yaml ``` ## Validating the specification file Let's validate the spec using both the Speakeasy CLI and OpenAPI Generator. ### Validation using Speakeasy Run the following command in the terminal where the specification file is located: ```bash speakeasy validate openapi -s petstore.yaml ``` The Speakeasy validator returns the following: ```bash ╭────────────╮╭───────────────╮╭────────────╮ │ Errors (0) ││ Warnings (10) ││ Hints (72) │ ├────────────┴┘ └┴────────────┴────────────────────────────────────────────────────────────╮ │ │ │ │ Line 250: operation-success-response - operation `updatePetWithForm` must define at least a single │ │ │ `2xx` or `3xx` response │ │ │ │ Line 277: operation-success-response - operation `deletePet` must define at least a single `2xx` or │ │ `3xx` response │ │ │ │ Line 413: operation-success-response - operation `deleteOrder` must define at least a single `2xx` o │ │ r │ │ `3xx` response │ │ │ │ Line 437: operation-success-response - operation `createUser` must define at least a single `2xx` or │ │ `3xx` response │ │ │ │ Line 524: operation-success-response - operation `logoutUser` must define at least a single `2xx` or │ │ `3xx` response │ │ │ │ •• │ └────────────────────────────────────────────────────────────────────────────────────────────────────────┘ ←/→ switch tabs ↑/↓ navigate ↵ inspect esc quit ``` The Speakeasy CLI validation result gives us a handy tool for switching between the errors, warnings, and hints tabs with the option to navigate through the results on each tab. In this instance, Speakeasy generated ten warnings. Let's correct them before continuing. Notice that some of the warnings contain a `default` response. For completeness, we'd like to explicitly return a `200` HTTP response. We'll make the following modifications in the `petstore.yaml` file. When the `updatePetWithForm` operation executes successfully, we expect an HTTP `200` response with the updated `Pet` object to be returned. Insert the following after `responses` on line 250: ``` "200": description: successful operation content: application/xml: schema: $ref: '#/components/schemas/Pet' application/json: schema: $ref: '#/components/schemas/Pet' ``` Similarly, following successful `createUser` and `updateUser` operations, we'd like to return an HTTP `200` response with a `User` object. Add the following text to both operations below `responses`: ``` "200": description: successful operation content: application/xml: schema: $ref: '#/components/schemas/User' application/json: schema: $ref: '#/components/schemas/User' ``` Now we'll add the same response to four operations. Copy the following text: ``` "200": description: successful operation ``` Paste this response after `responses` for the following operations: - `deletePet` - `deleteOrder` - `logoutUser` - `deleteUser` We are left with three warnings indicating potentially unused or orphaned objects and operations. For unused objects, locate the following lines of code and delete them: ``` Customer: type: object properties: id: type: integer format: int64 example: 100000 username: type: string example: fehguy address: type: array xml: name: addresses wrapped: true items: $ref: '#/components/schemas/Address' xml: name: customer Address: type: object properties: street: type: string example: 437 Lytton city: type: string example: Palo Alto state: type: string example: CA zip: type: string example: "94301" xml: name: address ``` To remove the unused request bodies, locate the following lines and delete them: ``` requestBodies: Pet: description: Pet object that needs to be added to the store content: application/json: schema: $ref: '#/components/schemas/Pet' application/xml: schema: $ref: '#/components/schemas/Pet' UserArray: description: List of user object content: application/json: schema: type: array items: $ref: '#/components/schemas/User' ``` Now if you validate the file with the Speakeasy CLI, you'll notice there are no warnings: ``` ╭────────────╮╭──────────────╮╭────────────╮ │ Errors (0) ││ Warnings (0) ││ Hints (75) │ ├────────────┴┴──────────────┴┘ └─────────────────────────────────────────────────────────────╮ │ │ │ │ Line 51: missing-examples - Missing example for requestBody. Consider adding an example │ │ │ │ Line 54: missing-examples - Missing example for requestBody. Consider adding an example │ │ │ │ Line 57: missing-examples - Missing example for requestBody. Consider adding an example │ │ │ │ Line 65: missing-examples - Missing example for responses. Consider adding an example │ │ │ │ Line 68: missing-examples - Missing example for responses. Consider adding an example │ │ │ │ ••••••••••••••• │ └────────────────────────────────────────────────────────────────────────────────────────────────────────┘ ←/→ switch tabs ↑/↓ navigate ↵ inspect esc quit ``` ### Validation using OpenAPI Generator To validate the `petstore.yaml` specification file with OpenAPI Generator, run the following command in the terminal: ```bash openapi-generator validate -i petstore.yaml ``` The OpenAPI Generator returns the following response, indicating no issues detected. ```bash Validating spec (petstore.yaml) No validation issues detected. ``` Now that we have made the `petstore.yaml` file more complete by fixing the warnings, let's use it to create SDKs. ## Creating SDKs We'll create Java SDKs using Speakeasy and OpenAPI Generator and then compare them. ### Creating an SDK with Speakeasy Create a Java SDK from the `petstore.yaml` specification file using Speakeasy by running the following command in the terminal: ```bash # Generate Petstore SDK using Speakeasy java generator speakeasy quickstart ``` The generator will return some logging results while the SDK is being created and a success indicator on completion. ```bash SDK for java generated successfully ✓ ``` ### Creating an SDK with OpenAPI Generator Run the following command in the terminal to generate a Java SDK using OpenAPI Generator: ```bash # Generate Petstore SDK using python generator openapi-generator generate \ --input-spec petstore.yaml \ --generator-name java \ --output ./sdks/petstore-sdk-java \ --additional-properties=packageName=petstore_sdk,projectName=petstore-sdk-java ``` The generator returns various logs and finally a successful generation message. ```bash ################################################################################ # Thanks for using OpenAPI Generator. # # Please consider donation to help us maintain this project 🙏 # # https://opencollective.com/openapi_generator/donate # ################################################################################ ``` ## SDK code compared: Project structure Let's compare the two project structures by printing a tree structure of each SDK directory's `src` folder. Run the following command to get the Speakeasy SDK structure: ```bash cd petstore-sdk-java-speakeasy/src/main/java tree ``` The results of the project structure are displayed as follows: ```bash ||____org | |____openapis | | |____openapi | | | |____Pet.java | | | |____SecuritySource.java | | | |____User.java | | | |____utils | | | | |____SpeakeasyMetadata.java | | | | |____SecurityMetadata.java | | | | |____LazySingletonValue.java | | | | |____RetryConfig.java | | | | |____TypedObject.java | | | | |____BigDecimalString.java | | | | |____Response.java | | | | |____OneOfDeserializer.java | | | | |____MultipartFormMetadata.java | | | | |____JSON.java | | | | |____Hooks.java | | | | |____Deserializers.java | | | | |____QueryParameters.java | | | | |____Utils.java | | | | |____QueryParamsMetadata.java | | | | |____Retries.java | | | | |____RequestBody.java | | | | |____RequestMetadata.java | | | | |____Security.java | | | | |____Metadata.java | | | | |____SpeakeasyHTTPClient.java | | | | |____BackoffStrategy.java | | | | |____SerializedBody.java | | | | |____Types.java | | | | |____HTTPClient.java | | | | |____Options.java | | | | |____HeaderMetadata.java | | | | |____PathParamsMetadata.java | | | | |____FormMetadata.java | | | | |____Hook.java | | | | |____HTTPRequest.java | | | | |____BigIntegerString.java | | | |____models | | | | |____operations | | | | | |____DeletePetRequest.java | | | | | |____GetPetByIdSecurity.java | | | | | |____UpdateUserFormResponse.java | | | | | |____CreateUserFormResponse.java | | | | | |____LoginUserRequestBuilder.java | | | | | |____UpdateUserRawRequestBuilder.java | | | | | |____DeletePetResponse.java | | | | | |____GetOrderByIdRequestBuilder.java | | | | | |____SDKMethodInterfaces.java | | | | | |____UpdateUserJsonRequestBuilder.java | | | | | |____Status.java | | | | | |____FindPetsByStatusRequest.java | | | | | |____DeleteOrderRequestBuilder.java | | | | | |____CreateUserJsonResponse.java | | | | | |____UpdateUserJsonResponse.java | | | | | |____DeleteOrderRequest.java | | | | | |____UpdateUserRawResponse.java | | | | | |____UpdatePetFormResponse.java | | | | | |____PlaceOrderJsonRequestBuilder.java | | | | | |____AddPetFormResponse.java | | | | | |____PlaceOrderRawRequestBuilder.java | | | | | |____UpdatePetJsonRequestBuilder.java | | | | | |____FindPetsByStatusRequestBuilder.java | | | | | |____CreateUserRawRequestBuilder.java | | | | | |____LoginUserRequest.java | | | | | |____FindPetsByTagsRequestBuilder.java | | | | | |____FindPetsByTagsRequest.java | | | | | |____LogoutUserResponse.java | | | | | |____FindPetsByStatusResponse.java | | | | | |____DeleteUserRequest.java | | | | | |____UpdateUserRawRequest.java | | | | | |____AddPetFormRequestBuilder.java | | | | | |____GetInventorySecurity.java | | | | | |____DeleteUserRequestBuilder.java | | | | | |____CreateUsersWithListInputResponse.java | | | | | |____DeleteOrderResponse.java | | | | | |____UpdateUserJsonRequest.java | | | | | |____GetPetByIdRequestBuilder.java | | | | | |____CreateUserFormRequestBuilder.java | | | | | |____CreateUserRawResponse.java | | | | | |____AddPetJsonResponse.java | | | | | |____UpdatePetJsonResponse.java | | | | | |____GetOrderByIdResponse.java | | | | | |____UploadFileResponse.java | | | | | |____DeletePetRequestBuilder.java | | | | | |____UpdatePetWithFormResponse.java | | | | | |____PlaceOrderJsonResponse.java | | | | | |____UpdateUserFormRequestBuilder.java | | | | | |____LoginUserResponse.java | | | | | |____UploadFileRequest.java | | | | | |____LogoutUserRequestBuilder.java | | | | | |____FindPetsByTagsResponse.java | | | | | |____GetPetByIdResponse.java | | | | | |____UpdatePetWithFormRequest.java | | | | | |____GetPetByIdRequest.java | | | | | |____UpdatePetRawResponse.java | | | | | |____CreateUsersWithListInputRequestBuilder.java | | | | | |____AddPetRawResponse.java | | | | | |____PlaceOrderFormResponse.java | | | | | |____GetUserByNameResponse.java | | | | | |____UpdatePetWithFormRequestBuilder.java | | | | | |____GetOrderByIdRequest.java | | | | | |____GetInventoryResponse.java | | | | | |____PlaceOrderFormRequestBuilder.java | | | | | |____UploadFileRequestBuilder.java | | | | | |____GetInventoryRequestBuilder.java | | | | | |____UpdatePetFormRequestBuilder.java | | | | | |____UpdatePetRawRequestBuilder.java | | | | | |____DeleteUserResponse.java | | | | | |____CreateUserJsonRequestBuilder.java | | | | | |____GetUserByNameRequest.java | | | | | |____AddPetJsonRequestBuilder.java | | | | | |____AddPetRawRequestBuilder.java | | | | | |____GetUserByNameRequestBuilder.java | | | | | |____UpdateUserFormRequest.java | | | | | |____PlaceOrderRawResponse.java | | | | |____components | | | | | |____Order.java | | | | | |____Status.java | | | | | |____Tag.java | | | | | |____ApiResponse.java | | | | | |____Pet.java | | | | | |____OrderStatus.java | | | | | |____Category.java | | | | | |____User.java | | | | | |____Security.java | | | | |____errors | | | | | |____SDKError.java | | | |____Store.java | | | |____SDKConfiguration.java | | | |____SDK.java ``` Now run the following command for the OpenAPI Generator SDK folder: ```bash cd petstore-sdk-java/src/main/java tree ``` The OpenAPI Generator SDK structure looks like this: ```bash |____org | |____openapitools | | |____client | | | |____ApiClient.java | | | |____ApiException.java | | | |____ProgressResponseBody.java | | | |____Pair.java | | | |____GzipRequestInterceptor.java | | | |____auth | | | | |____RetryingOAuth.java | | | | |____HttpBasicAuth.java | | | | |____ApiKeyAuth.java | | | | |____OAuth.java | | | | |____OAuthOkHttpClient.java | | | | |____Authentication.java | | | | |____OAuthFlow.java | | | | |____HttpBearerAuth.java | | | |____ApiResponse.java | | | |____JSON.java | | | |____ServerVariable.java | | | |____StringUtil.java | | | |____Configuration.java | | | |____ServerConfiguration.java | | | |____model | | | | |____Order.java | | | | |____ModelApiResponse.java | | | | |____Customer.java | | | | |____Tag.java | | | | |____Pet.java | | | | |____AbstractOpenApiSchema.java | | | | |____Category.java | | | | |____User.java | | | | |____Address.java | | | |____api | | | | |____PetApi.java | | | | |____UserApi.java | | | | |____StoreApi.java | | | |____ApiCallback.java | | | |____ProgressRequestBody.java ``` The Speakeasy-created SDK contains more generated files than the SDK from OpenAPI Generator, which is partly due to the Speakeasy SDK being less dependent on third-party libraries. ## Model and usage Let's take a look at how Speakeasy generates model classes for creating and updating a `Pet` object. ```java Pet req = Pet.builder() .name("Snoopie") .photoUrls(java.util.List.of( "https://some_url.com/snoopie1.jpg")) .id(1) .category(Category.builder() .id(1) .name("Dogs") .build()) .tags(java.util.List.of( Tag.builder() .build())) .status(Status.AVAILABLE) .build(); UpdatePetJsonResponse res = sdk.pet().updatePetJson() .request(req) .call(); if (res.body().isPresent()) { // handle response } ``` The Speakeasy model follows the builder pattern to construct objects with many optional parameters, making the code more readable and easier to use. Let's see how the OpenAPI Generator SDK performs the same operation: ```java ApiClient defaultClient = Configuration.getDefaultApiClient(); defaultClient.setBasePath("/api/v3"); // Configure OAuth2 access token for authorization: petstore_auth OAuth petstore_auth = (OAuth) defaultClient.getAuthentication("petstore_auth"); petstore_auth.setAccessToken("YOUR ACCESS TOKEN"); PetApi apiInstance = new PetApi(defaultClient); Pet pet = new Pet(); // Pet | Create a new pet in the store pet.setName("Snoopie"); try { Pet result = apiInstance.addPet(pet); System.out.println(result); } catch (ApiException e) { System.err.println("Exception when calling PetApi#addPet"); System.err.println("Status code: " + e.getCode()); System.err.println("Reason: " + e.getResponseBody()); System.err.println("Response headers: " + e.getResponseHeaders()); e.printStackTrace(); } ``` The OpenAPI Generator SDK focuses on manual serialization and deserialization using Gson, providing setter methods for individual properties of the `Pet` object. The two SDKs have distinctly different approaches to handling object creation, validation, and JSON serialization, with the Speakeasy-generated SDK emphasizing fluid and declarative object creation using modern patterns and annotations for handling JSON data. Let's look more closely at how the `Pet` model attributes are declared in each SDK. Notice how the Speakeasy SDK uses Jackson annotations for the JSON serialization and deserialization of objects. ```java public class Pet { @JsonInclude(Include.NON_ABSENT) @JsonProperty("id") @SpeakeasyMetadata("form:name=id") private Optional id; @JsonProperty("name") @SpeakeasyMetadata("form:name=name") private String name; @JsonInclude(Include.NON_ABSENT) @JsonProperty("category") @SpeakeasyMetadata("form:name=x") private Optional category; @JsonProperty("photoUrls") @SpeakeasyMetadata("form:name=photoUrls") private java.util.List photoUrls; //Rest of Pet.java .... ``` Compare this to the OpenAPI Generator SDK `Pet` model that uses Gson annotations: ```java public class Pet { public static final String SERIALIZED_NAME_ID = "id"; @SerializedName(SERIALIZED_NAME_ID) private Long id; public static final String SERIALIZED_NAME_NAME = "name"; @SerializedName(SERIALIZED_NAME_NAME) private String name; public static final String SERIALIZED_NAME_CATEGORY = "category"; @SerializedName(SERIALIZED_NAME_CATEGORY) private Category category; public static final String SERIALIZED_NAME_PHOTO_URLS = "photoUrls"; @SerializedName(SERIALIZED_NAME_PHOTO_URLS) private List photoUrls = new ArrayList<>(); //Rest of Pet.java .... ``` Let's take a moment and identify what the differences are between the Jackson vs GSON libraries and what features each has. The Gson JSON library is easy to use and implement and well-suited to smaller projects. It provides an API for JSON support but doesn't support extensive configuration options. On the other hand, Jackson is designed to be more configurable and flexible when it comes to JSON serialization and deserialization. Jackson is the standard JSON-support library in many popular Java frameworks (like Spring, Jersey, and RESTEasy), it's widely used in the Java community, and it's actively supported and frequently updated. Jackson is also generally faster and offers extensive configuration options. The use of the Jackson library in the Speakeasy-generated SDK provides us with a firm foundation for building fast and scalable applications. ## HTTP communication Java 11 (the minimum version supported by Speakeasy) significantly improved HTTP communication with the java.net.http package providing a powerful `HTTPClient` class for enhanced HTTP communication. Given the OpenAPI Generator SDK is Java 8 compatible, we suspected it might use some third-party libraries. On inspection, our suspicions were confirmed: The SDK uses a third-party library to handle HTTP communication. Take a look at the following method to add a new `Pet` object (from the `PetApi.java` file): ```java public Pet addPet(Pet pet) throws ApiException { ApiResponse localVarResp = addPetWithHttpInfo(pet); return localVarResp.getData(); } ``` The `addPet` method in turn calls the `addPetWithHttpInfo(pet)` method: ```java public ApiResponse addPetWithHttpInfo(Pet pet) throws ApiException { okhttp3.Call localVarCall = addPetValidateBeforeCall(pet, null); Type localVarReturnType = new TypeToken(){}.getType(); return localVarApiClient.execute(localVarCall, localVarReturnType); } ``` Note how the method uses the `okhttp3.Call` object. We examined the dependencies configured in the `build.gradle` file and discovered the `okhttp` dependency: ```groovy implementation 'com.squareup.okhttp3:okhttp:4.10.0' ``` Having established that the OpenAPI Generator SDK uses the OkHttp library, we were curious to see how the Speakeasy-generated SDK handles HTTP communication. Take a look at this extract from the `addPetJson` method in the `Pet.java` file of the Speakeasy SDK: ```java HTTPRequest req = new HTTPRequest(); req.setMethod("POST"); req.setURL(url); Object _convertedRequest = Utils.convertToShape(request, Utils.JsonShape.DEFAULT, new TypeReference() {}); SerializedBody serializedRequestBody = org.openapis.openapi.utils.Utils.serializeRequestBody( _convertedRequest, "request", "json", false); if (serializedRequestBody == null) { throw new Exception("Request body is required"); } req.setBody(serializedRequestBody); req.addHeader("Accept", "application/json;q=1, application/xml;q=0"); req.addHeader("user-agent", this.sdkConfiguration.userAgent); HTTPClient client = org.openapis.openapi.utils.Utils.configureSecurityClient( this.sdkConfiguration.defaultClient, this.sdkConfiguration.securitySource.getSecurity()); HttpResponse httpRes = client.send(req); ``` This method uses `HTTPClient`, `HTTPRequest`, and `HTTPResponse` objects. If we look at the import statements, we can see that these objects are generated from the following classes: ```java import org.openapis.openapi.utils.HTTPClient; import org.openapis.openapi.utils.HTTPRequest; import java.net.http.HttpResponse; ``` The `HTTPClient` and `HTTPRequest` interfaces are both generated by Speakeasy. We can see the `HTTPClient` interface implemented in `SpeakeasyHTTPClient.java` to establish the HTTP communication method: ```java import org.openapis.openapi.utils.HTTPClient; import java.io.IOException; import java.net.URISyntaxException; import java.net.http.HttpClient; import java.net.http.HttpRequest; import java.net.http.HttpResponse; import java.io.InputStream; public class SpeakeasyHTTPClient implements HTTPClient { @Override public HttpResponse send(HttpRequest request) throws IOException, InterruptedException, URISyntaxException { HttpClient client = HttpClient.newHttpClient(); return client.send(request, HttpResponse.BodyHandlers.ofInputStream()); } } ``` The Speakeasy SDK uses the Java HTTP APIs that were introduced in Java 11. Some of the benefits of using the built-in Java HTTP APIs are: - **Standardization:** By using the HTTP Client API supported in Java 11, the SDK uses the standards provided and supported by modern Java SDK providers. The `HttpClient` class integrates more easily with the other Java APIs in the Java SDK. - **Asynchronous support:** Asynchronous HTTP communication is not available in Java 8, making it harder to build scalable applications. The HTTP Client API asynchronous HTTP communication available in Java 11 provides a CompletableFuture object immediately after calling the API, which gives developers more control. - **Performance and efficiency:** The HTTP Client is created using a builder and allows for configuring client-specific settings, such as the preferred protocol version (HTTP/1.1 or HTTP/2). It also supports Observable APIs. - **Security, stability, and long-term support:** As a standard Java API, the HTTP Client is more stable and secure, and benefits from the long-term support cycles of new Java versions. ## Retries The SDK created by Speakeasy can automatically retry requests. You can enable retries globally or per request using the `x-speakeasy-retries` extension in your OpenAPI specification document. Let's add the `x-speakeasy-retries` extension to the `addPet` method in the `petstore.yaml` file: ```yaml # ... paths: /pet: # ... post: #... operationId: addPet x-speakeasy-retries: strategy: backoff backoff: initialInterval: 500 # 500 milliseconds maxInterval: 60000 # 60 seconds maxElapsedTime: 3600000 # 5 minutes exponent: 1.5 statusCodes: - 5XX retryConnectionErrors: true ``` If you re-generate the SDK now, the new retry configuration will be included. For more information on configuring retries in your SDK, take a look at the [retries documentation](/docs/customize-sdks/retries). ## SDK dependencies Let's compare dependencies in the two SDKs. Here are the OpenAPI Generator SDK dependencies in `build.gradle`: ```groovy implementation 'io.swagger:swagger-annotations:1.6.8' implementation "com.google.code.findbugs:jsr305:3.0.2" implementation 'com.squareup.okhttp3:okhttp:4.10.0' implementation 'com.squareup.okhttp3:logging-interceptor:4.10.0' implementation 'com.google.code.gson:gson:2.9.1' implementation 'io.gsonfire:gson-fire:1.9.0' implementation 'javax.ws.rs:jsr311-api:1.1.1' implementation 'javax.ws.rs:javax.ws.rs-api:2.1.1' implementation 'org.openapitools:jackson-databind-nullable:0.2.6' implementation group: 'org.apache.oltu.oauth2', name: 'org.apache.oltu.oauth2.client', version: '1.0.2' implementation group: 'org.apache.commons', name: 'commons-lang3', version: '3.12.0' implementation "jakarta.annotation:jakarta.annotation-api:$jakarta_annotation_version" testImplementation 'org.junit.jupiter:junit-jupiter-api:5.9.1' testImplementation 'org.mockito:mockito-core:3.12.4' testRuntimeOnly 'org.junit.jupiter:junit-jupiter-engine:5.9.1' ``` Here are the Speakeasy SDK dependencies from `build.gradle`: ``` implementation 'com.fasterxml.jackson.core:jackson-databind:2.16.2' implementation 'com.fasterxml.jackson.datatype:jackson-datatype-jsr310:2.16.2' implementation 'com.fasterxml.jackson.datatype:jackson-datatype-jdk8:2.16.2' implementation 'org.openapitools:jackson-databind-nullable:0.2.6' implementation 'org.apache.httpcomponents:httpclient:4.5.14' implementation 'org.apache.httpcomponents:httpmime:4.5.14' implementation 'com.jayway.jsonpath:json-path:2.9.0' implementation 'commons-io:commons-io:2.15.1' ``` The OpenAPI Generator SDK implements more libraries than the Speakeasy SDK, possibly due to the compatibility requirements and limitations of Java 8. Depending on fewer third-party implementations provides the Speakeasy SDK with some advantages: - **Less maintenance:** Projects with fewer dependencies have a lower maintenance overhead and less versioning to keep track of long term. - **Reduced risk of dependency-related issues:** Third-party dependencies increase the risk of bugs and security failures that depend on the third-party provider to fix. A security flaw in a third-party dependency makes your application vulnerable. - **Improved performance:** Code generally works better in standard Java APIs as they have been through rigorous testing and QA cycles before being made available to the public. - **Easier adoption:** Projects tend to more readily accept SDK builds that rely on fewer third-party dependencies, due to strict policies regarding the use and management of these dependencies. ## Handling non-nullable fields Let's see how Speakeasy's enhanced null safety and Optional support work on fields in the `Pet` object. Take a look at the following declaration taken from the `Pet` object in the Speakeasy SDK `org.openapis.openapi.models.components.Pet.java` file: ```java @JsonInclude(Include.NON_ABSENT) @JsonProperty("status") @SpeakeasyMetadata("form:name=status") private Optional status; ``` Note that the `@JsonInclude` annotation indicates it is `NON-ABSENT` and the `Optional` class is used. The status field here is an enum (`Status`) wrapped in the `Optional` class. Let's examine the `Status` enum object: ```java public enum Status { AVAILABLE("available"), PENDING("pending"), SOLD("sold"); @JsonValue private final String value; private Status(String value) { this.value = value; } public String value() { return value; } } ``` Let's compare the Speakeasy SDK `status` field declaration to the same field in the OpenAPI Generator SDK. The following declaration is taken from the `org.openapitools.client.model.Pet.java` file: ```java public enum StatusEnum { AVAILABLE("available"), PENDING("pending"), SOLD("sold"); private String value; StatusEnum(String value) { this.value = value; } public String getValue() { return value; } @Override public String toString() { return String.valueOf(value); } public static StatusEnum fromValue(String value) { for (StatusEnum b : StatusEnum.values()) { if (b.value.equals(value)) { return b; } } throw new IllegalArgumentException("Unexpected value '" + value + "'"); } public static class Adapter extends TypeAdapter { @Override public void write(final JsonWriter jsonWriter, final StatusEnum enumeration) throws IOException { jsonWriter.value(enumeration.getValue()); } @Override public StatusEnum read(final JsonReader jsonReader) throws IOException { String value = jsonReader.nextString(); return StatusEnum.fromValue(value); } } public static void validateJsonElement(JsonElement jsonElement) throws IOException { String value = jsonElement.getAsString(); StatusEnum.fromValue(value); } } public static final String SERIALIZED_NAME_STATUS = "status"; @SerializedName(SERIALIZED_NAME_STATUS) private StatusEnum status; ``` At first glance, the OpenAPI Generator SDK also uses an enum approach, representing the `status` field as an enum called `StatusEnum` with three possible values: `AVAILABLE`, `PENDING`, and `SOLD`. A lot of code is generated around this field to handle the enum, but the `Pet` object does not indicate that the OpenAPI Generator SDK `status` field is non-nullable at this point. In contrast, Speakeasy uses a direct approach to non-nullable fields. The Speakeasy SDK also uses a `Status` enum for the `status` field, but it is wrapped in the `Optional` class provided by the Java standard APIs. Declaring the `status` field as the `Status` type wrapped in the `Optional` class has some benefits to the developer: - It helps to avoid possible `NullPointerException` errors when accessing a `null` value. - It provides a modern way for developers to identify the absence of a value using the `isPresent()` method from the `Optional` class API and exposes other usable methods like `orElse()` and `orElseThrow()`. - It clearly states the intent and use of the code, which helps to reduce bugs in the long run. Let's see how client validation works by passing a `null` value to the `findPetsByStatus()` method, which expects `Optional status`. We create the following builder pattern for a new request: ```java FindPetsByStatusResponse res = sdk.pet().findPetsByStatus(null); if (res.body().isPresent()) { // handle response } ``` When we execute this bit of code, we get the following exception: ``` java.lang.IllegalArgumentException: status cannot be null at org.openapis.openapi.utils.Utils.checkNotNull(Utils.java:469) at org.openapis.openapi.models.operations.FindPetsByStatusRequest$Builder.status(FindPetsByStatusRequest.java:108) at org.openapis.openapi.Pet.findPetsByStatus(Pet.java:503) ``` The same exception is generated if we remove the `name` field from the `builder` declaration of a new `Pet` object: ```java Pet req = Pet.builder() .id(1) .photoUrls(java.util.List.of( "https://hips.hearstapps.com/hmg-prod/images/dog-puppy-on-garden-royalty-free-image-1586966191.jpg?crop=1xw:0.74975xh;center,top&resize=1200:*")) .category(Category.builder() .id(1) .name("Dogs") .build()) .tags(java.util.List.of( Tag.builder() .build())) .build(); ``` When we execute the above code, we get the exception: ```bash java.lang.IllegalArgumentException: name cannot be null at org.openapis.openapi.utils.Utils.checkNotNull(Utils.java:469) at org.openapis.openapi.models.components.Pet.(Pet.java:62) at org.openapis.openapi.models.components.Pet$Builder.build(Pet.java:297) at org.openapis.openapi.Test.main(Test.java:40) ``` The null check validation generates this exception when the `Pet` object is initiated and certain values are null. If we look at the class constructor in our `Pet.java` model in the Speakeasy SDK: ```java public Pet( @JsonProperty("id") Optional id, @JsonProperty("name") String name, @JsonProperty("category") Optional category, @JsonProperty("photoUrls") java.util.List photoUrls, @JsonProperty("tags") Optional> tags, @JsonProperty("status") Optional status) { Utils.checkNotNull(id, "id"); Utils.checkNotNull(name, "name"); Utils.checkNotNull(category, "category"); Utils.checkNotNull(photoUrls, "photoUrls"); Utils.checkNotNull(tags, "tags"); Utils.checkNotNull(status, "status"); this.id = id; this.name = name; this.category = category; this.photoUrls = photoUrls; this.tags = tags; this.status = status; } ``` We can see that the exception is generated in the `Utils.checkNotNull()` method: ```java public static T checkNotNull(T object, String name) { if (object == null) { // IAE better than NPE in this use-case (NPE can suggest internal troubles) throw new IllegalArgumentException(name + " cannot be null"); } return object; } ``` Therefore, if we omit the `name` field or pass a `null` value in the `findPetByStatus()` method, an exception is generated by the check null validation because the `name` and `status` fields explicitly set to `null` in this case. Let's try creating a `Pet` object without a `name` field using the OpenAPI Generator SDK: ```java PetApi apiInstance = new PetApi(defaultClient); ArrayList snoopyPhotos = new ArrayList<>(); snoopyPhotos.add("https://Snoopy.some_photo_platform.com"); Pet pet = new Pet(); // Pet | Create a new pet in the store pet.setPhotoUrls(snoopyPhotos); try { Pet result = apiInstance.addPet(pet); } catch (ApiException e) { //handle exception } ``` When we execute the above code, we get a mixed result. The following exception is generated: ``` Exception in thread "main" java.lang.IllegalArgumentException: The required field `name` is not found in the JSON string: {"id":9223372036854775807,"photoUrls":["https://Snoopy.some_photo_platform.com"],"tags":[]} at org.openapitools.client.model.Pet.validateJsonElement(Pet.java:361) at org.openapitools.client.model.Pet$CustomTypeAdapterFactory$1.read(Pet.java:422) at org.openapitools.client.model.Pet$CustomTypeAdapterFactory$1.read(Pet.java:412) at com.google.gson.TypeAdapter$1.read(TypeAdapter.java:204) .... ``` It appears that the `Pet` object was created on the API, but the call failed retrospectively on the client side. The exception was generated by the SDK's validation process, which checks the JSON response received from the API. You can see the created object in the JSON response included in the exception: ```json {"id":9223372036854775807,"photoUrls":["https://Snoopy.some_photo_platform.com"],"tags":[]} ``` Validation failed because the name was missing from the JSON string. This validation method is not helpful, as it checks the response after the API call rather than before the request is sent. Consequently, an invalid object was created on the API and the client process failed. Speakeasy's proactive client validation and method of handling non-nullable fields with the use of the `Optional` class is elegant. Code that is easy to read, understand, and use, and that also helps to build null safety is essential for building robust, maintainable SDKs. ## Generated documentation Both Speakeasy and OpenAPI Generator generate SDK documentation for the generated code. Each generator creates a README file to help users get started with the SDK. The OpenAPI Generator README outlines the SDK's compatibility with Java, Maven, and Gradle versions and identifies the available API routes. The Speakeasy README file is more complete and documents more examples. Speakeasy also generates additional documentation in the `docs` directory, including more detailed explanations of the models and operations; examples of creating, updating, and searching objects; error handling; and guidance on handling exceptions specific to the OpenAPI specification file. A handy "Getting Started" section details how to build the SDK. In general, we found the Speakeasy documentation to be more complete and helpful. We tested many API call examples from the documentation, and conclude that the Speakeasy docs are production-ready. ## Supported Java versions The Speakeasy-generated SDK supports Java 11+ environments, and the SDK generated by OpenAPI Generator supports Java 8+. While the OpenAPI SDK supports more codebases including those still using Java 8, the Speakeasy SDK leverages the enhancements provided by Java 11. Java 8 was released in March 2014, and was the most widely used version of Java until version 11 was released in September 2019. In 2023, New Relic reported that Java 11 is used in around 56% of production applications, while Java 8 is still in use at around 33%. Both versions are important long-term support (LTS) versions. ## Summary We've seen how easy it is to generate a powerful, idiomatic SDK for Java using Speakeasy. If you're building an API that developers rely on and would like to publish full-featured Java SDKs that follow best practices, we highly recommend giving the Speakeasy SDK generator a try. For more customization options for your SDK using the Speakeasy generator, please see the [Speakeasy documentation](/docs/customize-sdks). Join our Slack community to let us know how we can improve our Java SDK generator or suggest features. # SDK Feature Matrix by Category Source: https://speakeasy.com/docs/sdks/languages/maturity import { Table } from "@/mdx/components"; ## Maturity levels
## Feature support levels Feature support levels indicate the extent of additional functionalities provided.
## Deprecated generation targets * TypeScript Beta (v1) * Java Beta (v1) This document outlines the OpenAPI and SDK features supported by Speakeasy. Features are grouped by category to help you quickly locate what's available per SDK. **Legend**: ✅ Implemented, ⚠️ Partially Implemented (missing Readme sections or tests), ⛔ Not Implemented, ➖ Ignored** _Note: This is not a complete list. Some SDK features are language-specific or not yet documented here._ ## Customization Basics
## Structure
## Data Model
## Customize Methods
## Responses & Error Handling
## Global Parameters
## Configure Servers
## Security & Authentication
## SDK Behavior
## Add Webhooks
## Add Custom Code
## Environment
## Documentation & Dev Experience
# Our design philosophy Source: https://speakeasy.com/docs/sdks/languages/philosophy import { supportedLanguages, ossComparisonData } from "@/lib/data/docs/languages"; import { CardGrid } from "@/components/card-grid"; SDKs are a critical interface for an API, which is why we put a lot of thought into developer experience. Our philosophy is that SDKs should be: - **Type safe:** The SDKs we generate are fully typed, ensuring customers can catch errors early and often. - **Human readable:** The SDKs we generate are easy for developers to read and debug in the customer's IDE. We avoid convoluted abstractions. - **Batteries-included:** The SDKs we generate include everything from telemetry and retries to pagination. - **Forward compatible:** Generated SDKs handle API evolution gracefully. Enums and unions are open to new values so that new versions of your API don't break old clients. - **Fault tolerant:** Our generator is easy to use and outputs usable SDKs wherever possible. If a working SDK cannot be output, the OpenAPI document will be validated and any problems will be flagged. Generated SDKs also deserialize missing values gracefully while ensuring type safe SDKs. - **Capable of going beyond OpenAPI:** Our generator covers the OpenAPI Specification and can extend where OpenAPI falls short. - **Lean:** Our SDKs are meant to be powerful but lean, with minimal dependencies. We start with native language libraries and layer on third-party libraries only when the customer benefits far outweigh the cost of the extra dependency. We avoid adding unnecessary dependencies. Please see the table below for language-specific details. ## OSS Comparisons See how our SDKs compare to other open-source alternatives: # Create PHP SDKs from OpenAPI documents Source: https://speakeasy.com/docs/sdks/languages/php/methodology-php import { FileTree } from "nextra/components"; ## PHP SDK overview The Speakeasy PHP SDK is designed to be easy to use and debug, and uses object-oriented programming in PHP for a robust and strongly-typed experience. Some core features of the SDK are: - Class-based objects using reflection and property attributes to aid serialization. - A `Utils` package for common operations, simplifying generated code and making it easier to debug. - A convenient builder pattern manages the SDK configuration, and request level options. - Support for some OAuth flows and other standard security mechanisms. ## New PHP features Since the release of PHP 8 in 2020, the language has introduced additional type features, enabling better support for OpenAPI. Some of the features we take advantage of are: - Union types ```php private int|float $age; - Enums ```php enum HTTPMethods: string { case GET = 'get'; case POST = 'post'; } ``` ## External libraries The Speakeasy PHP SDK seeks to support the majority of the OpenAPI Specification features, and as such, supports some features that aren't contained in the PHP standard library. Speakeasy fills the gaps using some external dependencies, which are detailed below. ### Dates PHP has only date-time objects, not date objects. Speakeasy uses [Brick\DateTime](https://github.com/brick/date-time) for date support. For example: ```php public function deserializeDateTimeToJson(JsonDeserializationVisitor $visitor, string $data, array $type, Context $context): mixed { return \Brick\DateTime\LocalDate::parse($data); } ``` ### Complex numbers PHP doesn't have support for arbitrary precision numbers, so we use the [Brick\Math](https://github.com/brick/math) library for complex number support. To learn more about Speakeasy's complex number support, please read [this page](/docs/customize/data-model/complex-numbers). ### HTTP client The SDK uses [Guzzle](https://docs.guzzlephp.org/en/stable) to provide a default HTTP client implementation, `\GuzzleHttp\Client`, for making API calls, which can be overridden. The client must implement the `\GuzzleHttp\ClientInterface`. To override the HTTP client, pass the client during construction: ```php use GuzzleHttp\Client; $client = new Client([ 'timeout' => 2.0, ]); $sdk = SDK::builder()->setClient( $client )->build(); ``` This allows for full customization of low-level features, like proxies, custom headers, timeouts, cookies, and others. ### Exhaustive type system Speakeasy uses a combination of the [phpDocumentor TypeResolver](https://github.com/phpDocumentor/TypeResolver) library and the built-in standard library type specifications to provide exhaustive type checking across all aspects of the generated SDK. ### Serialization Speakeasy uses [JMS Serializer](https://jmsyst.com/libs/serializer) for serialization due to its union support, which other serialization libraries lack. JMS Serializer checks types received in responses at runtime, guaranteeing strong typing not only in comment annotations, but also while the application is in use and transferring data. Files in the Speakeasy-created PHP SDK include the line `declare(strict_types=1);`, which causes PHP to throw a `TypeError` if a function accepts or returns an invalid type at runtime. ### Type checking and linting Speakeasy uses a combination of [PHPStan](https://phpstan.org) and [Laravel Pint](https://laravel.com/docs/11.x/pint) for linting, performing quality control, and statically analyzing the SDK. ### Quality and security Speakeasy also uses [Roave Security Advisories](https://github.com/Roave/SecurityAdvisories) to ensure that its dependencies do not have any known security advisories. ### Tests [PHPUnit](https://phpunit.de/documentation.html) is included with the SDK for running tests. However, no tests are created for the SDK automatically. ## PHP SDK package structure ## PHP SDK data types and classes The Speakeasy PHP SDK uses native types wherever possible: - `string` - `DateTime` - `int` - `float` - `bool` Where no native data types are available, the Speakeasy PHP SDK uses libraries: - `Brick\DateTime\LocalDate` - `Brick\Math\BigInteger` - `Brick\Math\BigDecimal` The generated classes are standard PHP classes with public properties. These classes use attributes, docstrings, annotations and reflection to help guide serialization. ## Parameters When configured, Speakeasy will include up to a specified number of parameters directly in the function signatures, rather than providing the list of parameters as an object to be passed to the operation methods. The maximum number of parameters to be placed in the method signature is set in the `maxMethodParams` option in the `gen.yaml` file. If `maxMethodParams` is not set or is set to `0`, no method parameters will be added. ## Errors The Speakeasy PHP SDK throws exceptions using the appropriate `error` class as defined in the sdk specification. Wrap requests in a `try` block to handle the error in the response. ## User agent strings The PHP SDK includes a [user agent](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/User-Agent) string in all requests, which can be leveraged to track SDK usage amongst broader API usage. The format is as follows: ```stmpl speakeasy-sdk/php {{SDKVersion}} {{GenVersion}} {{DocVersion}} {{PackageName}} ``` - `SDKVersion` is the version of the SDK defined in `gen.yaml` and released. - `GenVersion` is the version of the Speakeasy generator. - `DocVersion` is the version of the OpenAPI document. - `PackageName` is the name of the package defined in `gen.yaml`. ## Feature examples Let's take a look at how OpenAPI features are mapped to PHP code. We'll use snippets from the [Swagger PetStore 3.1](https://petstore31.swagger.io/) OpenAPI document, [`openapi.yaml`](https://petstore31.swagger.io/api/v31/openapi.yaml). If you're not familiar with the example, it provides operations for managing users, customers, pets, and orders for pets in a hypothetical pet store. ### Tags Each `tag` in the OpenAPI document becomes one file of top-level operations, such as `Pet.php`, `Store.php`, and `User.php` for: ```yaml tags: - name: pet description: Everything about your Pets externalDocs: description: Find out more url: http://swagger.io ... ``` ### Security The Swagger Petstore OpenAPI document uses API key security and OAuth 2.0: ```yaml /pet/{petId}: security: - api_key: [] - petstore_auth: - write:pets - read:pets ... components: securitySchemes: petstore_auth: type: oauth2 flows: implicit: authorizationUrl: https://petstore31.swagger.io/oauth/authorize scopes: write:pets: modify pets in your account read:pets: read your pets api_key: type: apiKey name: api_key in: header ``` The PHP SDK creates a security class you can call with either scheme: ```php use OpenAPI\OpenAPI\Utils\SpeakeasyMetadata; class GetPetByIdSecurity { /** * * @var ?string $apiKey */ #[SpeakeasyMetadata('security:scheme=true,type=apiKey,subtype=header,name=api_key')] public ?string $apiKey = null; /** * * @var ?string $petstoreAuth */ #[SpeakeasyMetadata('security:scheme=true,type=oauth2,name=Authorization')] public ?string $petstoreAuth = null; /** * @param ?string $apiKey * @param ?string $petstoreAuth */ public function __construct(?string $apiKey = null, ?string $petstoreAuth = null) { $this->apiKey = $apiKey; $this->petstoreAuth = $petstoreAuth; } } # Example call: $requestSecurity = new Operations\GetPetByIdSecurity(); $requestSecurity->apiKey = ''; $response = $sdk->pet->getPetById($requestSecurity, 504151); ``` The implicit flow is the only OAuth flow currently supported. ### Enums Speakeasy uses native types in PHP 8 for enums. ```php enum Status: string { case Available = 'available'; case Pending = 'pending'; case Sold = 'sold'; } ``` ### Typed parameters Consider the following example of an array of strings in `openapi.yaml`: ```yaml /pet/findByTags: get: operationId: findPetsByTags parameters: - name: tags in: query required: false explode: true schema: type: array items: type: string ``` The PHP SDK types the parameter in a DocBlock. ```php /** Finds Pets by tags * @param ?array $tags * @return Operations\FindPetsByTagsResponse * @throws \OpenAPI\OpenAPI\Models\Errors\SDKException */ public function findPetsByTags(?array $tags = null,): Operations\FindPetsByTagsResponse { ``` You can use `oneOf` in an OpenAPI document like this: ```yaml Pet: type: object properties: age: oneOf: - type: integer - type: string ``` The `age` property will be typed as a union in PHP: ```php /** * * @param int|string|null $age */ ``` # OpenAPI PHP SDK creation: Speakeasy vs open source Source: https://speakeasy.com/docs/sdks/languages/php/oss-comparison-php import { Table } from "@/mdx/components"; Many of our users have switched from [OpenAPI Generator](https://openapi-generator.tech/) to Speakeasy for their PHP SDKs. Learn how to use both SDK creators in this guide, and the differences between them. Open-source OpenAPI generators are great for experimentation but lack the reliability, performance, and intuitive developer experience required for critical applications. As an alternative, Speakeasy creates [idiomatic SDKs](/post/client-sdks-as-a-service) that meet the bar for enterprise use. Here's the high-level summary of the differences between Speakeasy and OpenAPI Generator:
In this post, we'll do a technical deep dive on creating PHP SDKs using both Speakeasy and OpenAPI Generator, then we'll compare the generated SDKs. ## What is OpenAPI Generator? **OpenAPI Generator** (not to be confused with a generic **OpenAPI generator**) is a community-run, open-source tool for generating SDKs from OpenAPI specifications, with a [focus on version 3](https://openapi-generator.tech/docs/fork-qna). OpenAPI Generator originated as a fork of [Swagger Codegen](https://swagger.io/tools/swagger-codegen), a similar tool maintained by Smartbear. ## Preparing the SDK generators For our comparison, we ran Speakeasy and OpenAPI Generator in separate Docker containers, which work on Windows, macOS, and Linux. Using Docker instead of running code directly on your physical machine is safer, as the code cannot access files outside the folder you specify. We used the PetStore 3.1 YAML schema file from the [Swagger editor](https://editor-next.swagger.io) examples menu. To follow along with this guide, locate the PetStore file in **File -> Load Example -> OpenAPI 3.1 Petstore** and save it to a subfolder called `app` in your current path, such as `app/schema.yaml`. OpenAPI Generator provides a Docker image, but Speakeasy does not. To install the Speakeasy CLI, you can either follow the steps in the [Speakeasy Getting Started guide](/docs/speakeasy-reference/cli/getting-started) to install the Go binary directly on your computer, or run it in Docker, as we did. To use Docker, first create a `Dockerfile` with the content below, replacing `YourApiKey` with your key from the Speakeasy website. ```bash FROM alpine:3.19 WORKDIR /app RUN apk add bash go curl unzip sudo nodejs npm RUN curl -fsSL https://go.speakeasy.com/cli-install.sh | sh; ENV GOPATH=/root/go ENV PATH=$PATH:$GOPATH/bin ENV SPEAKEASY_API_KEY=YourApiKey ``` Then build the Speakeasy image with the command below. ```sh docker build -t seimage . ``` ## Validating the schemas Both OpenAPI Generator and the Speakeasy CLI can validate an OpenAPI schema. We'll run both and compare the output. ### Validation using OpenAPI Generator To validate `schema.yaml` using OpenAPI Generator, run the following in the terminal: ```bash docker run --rm -v "./app:/local" openapitools/openapi-generator-cli validate -i /local/schema.yaml ``` OpenAPI Generator returns two warnings: ``` Warnings: - Unused model: Address - Unused model: Customer [info] Spec has 2 recommendation(s). ``` ### Validation using Speakeasy Validate the schema with Speakeasy by running the following in the terminal: ```bash docker run --rm -v "./app:/app" seimage speakeasy validate openapi -s /app/schema.yaml ``` The Speakeasy validator returns 72 hints about missing examples, seven warnings about missing responses, and three warnings about unused components. Each warning includes a detailed JSON-formatted error with line numbers. Since both validators return only warnings and not errors, we can assume both generators will create SDKs without issues. ## Creating the SDKs First, we'll create an SDK with OpenAPI Generator, and then we'll create one with Speakeasy. ### Creating an SDK with OpenAPI Generator OpenAPI Generator includes three different PHP SDK creators (and six server creators). We'll use the stable [PHP creator](https://openapi-generator.tech/docs/generators/php), as the others are in beta testing and have fewer features. To create an SDK from the schema file using OpenAPI Generator, we ran the command below, which we found in the [OpenAPI Generator README](https://github.com/OpenAPITools/openapi-generator#16---docker). ```sh docker run --rm -v "./app:/local" openapitools/openapi-generator-cli generate -i /local/schema.yaml -g php -o /local/og ``` OpenAPI Generator creates three folders:
A warning from OpenAPI Generator in the terminal read: ``` Generation using 3.1.0 specs is in development and is not officially supported yet. ``` The [OpenAPI Generator roadmap](https://openapi-generator.tech/docs/roadmap) hasn't been updated in almost two years. ### Creating an SDK with Speakeasy Next, we'll create an SDK using the Speakeasy CLI with the command below. ```bash docker run --rm -v "./app:/app" seimage speakeasy quickstart ``` Speakeasy gives multiple warnings about `xml request bodies are not currently supported` and creates the following folders.
Speakeasy does not create test stubs, as unit testing is performed on Speakeasy's generator instead of the generated SDK. Shipping unit tests for generated SDKs adds unnecessary complexity and dependencies. ## Calling the server Swagger provides a complete test server for the PetStore OpenAPI 3.1 schema at https://petstore31.swagger.io. We called the pet operations given in each SDK's README file against the test server to check that the SDKs contain working code. We used a [Docker Composer 2.7](https://hub.docker.com/layers/library/composer/2.7/images/sha256-692dd0a0b775cc25ea0cf3ed936b1470647191a6417047e6a77d757a9f29c956?context=explore) container, which is based on Alpine 3 and PHP 8. ### Calling the server with the OpenAPI Generator SDK We used the `app/og/main.php` script below to call the API with the SDK generated by OpenAPI Generator. The example code was mostly given in the `README.md` file. ```php filename="app/og/main.php" setAccessToken('test'); $apiInstance = new OpenAPI\Client\Api\PetApi(new GuzzleHttp\Client(), $config); $pet = new \OpenAPI\Client\Model\Pet(); // \OpenAPI\Client\Model\Pet | Create a new pet in the store $pet->setId(1); $pet->setName("1"); try { $result = $apiInstance->addPet($pet); print_r($result); } catch (Exception $e) { echo 'Exception when calling PetApi->addPet: ', $e->getMessage(), PHP_EOL; } ``` To get access to the folder to create the script, give yourself permissions to the shared Docker volume with the command below, using your username. ```sh sudo chown -R yourUsername ./app ``` Next, we ran the command below and received a successful response. ```sh docker run --rm -v "./app/og:/app" -w "/app" composer:2.7 sh -c "composer install && php main.php" ``` The response of `$apiInstance->addPet($pet)` is below. ```text filename="Output" OpenAPI\Client\Model\Pet Object ( [openAPINullablesSetToNull:protected] => Array() [container:protected] => Array ( [id] => 1 [name] => 1 [category] => [photo_urls] => Array() [tags] => Array() [status] => ) ) ``` First, the command installs the PHP dependencies in the Docker container as recommended in the SDK `README.md` file, then it runs the sample `main.php` script to call the server using the SDK. ### Calling the server with the Speakeasy SDK The SDK Speakeasy creates also calls the server successfully. Below is an example script to call the API with the SDK created by Speakeasy. Save it as `app/se/main.php`. ```php filename="app/se/main.php" "); $sdk = OpenAPI\SDK::builder() ->setSecurity($security->petstoreAuth) ->build(); try { // Fully typed SDK objects $request = new Components\Pet10( name: 'doggie', photoUrls: [ 'https://example.com/doggie.jpg', 'https://example.com/doggie2.jpg', ], id: 10, tags: [ new Components\Tag( id: 123, name: 'pets', ), new Components\Tag( id: 3, name: 'good-dogs', ), new Components\Tag( id: 900, name: 'not-cats', ), ], // Typed subobjects category: new Components\Category( id: 1, name: 'Dogs', ), // Enums help you validate the input data status: Components\Status::Available ); $response = $sdk->pet->addPetForm($request); if ($response->pet !== null) { print_r($response->pet); } } catch (Throwable $e) { print_r($e); } ``` In the example above, we use the `Components` namespace to create a typed security object and a typed request object. We then call the `addPetForm` operation on the `pet` object in the SDK. You'll notice that the SDK helps you validate the input data with enums and typed subobjects. Let's run the script to see the response. The command to run the script is nearly identical to the command the OpenAPI Generator SDK used, except for using the Speakeasy folder. ```sh docker run --rm -v "./app/se:/app" -w "/app" composer:2.7 sh -c "composer install && php main.php" ``` The response of `$sdk->pet->addPetForm($request)` is below. ```text filename="Output" OpenAPI\OpenAPI\Models\Components\Pet15 Object ( [id] => 10 [name] => doggie [category] => OpenAPI\OpenAPI\Models\Components\Category Object ( [id] => 1 [name] => Dogs ) [photoUrls] => Array ( [0] => https://example.com/doggie2.jpg ) [tags] => Array ( [0] => OpenAPI\OpenAPI\Models\Components\Tag Object ( [id] => 3 [name] => good-dogs ) [1] => OpenAPI\OpenAPI\Models\Components\Tag Object ( [id] => 900 [name] => not-cats ) ) [status] => OpenAPI\OpenAPI\Models\Components\Status Enum:string ( [name] => Available [value] => available ) ) ``` ## Package structure Let's compare the structure of the SDKs in terms of code volume and folder structure. You can count the lines of code in the SDKs by running `cloc` for each (ignoring documentation and test folders): ```bash cloc ./app/og/lib cloc ./app/se/src ``` Below are the results for each SDK.
We see that the Speakeasy SDK has five times as many files as OpenAPI Generator, but 40% less code. The libraries Speakeasy uses, as well as shared utility functions, allow it to create more concise code than OpenAPI Generator. The following commands output the files of each SDK. ```sh tree ./app/og/lib tree ./app/se/src ``` Below is the output for OpenAPI Generator. ```sh ├── Api │ ├── PetApi.php │ ├── StoreApi.php │ └── UserApi.php ├── ApiException.php ├── Configuration.php ├── HeaderSelector.php ├── Model │ ├── Address.php │ ├── ApiResponse.php │ ├── Category.php │ ├── Customer.php │ ├── ModelInterface.php │ ├── Order.php │ ├── Pet.php │ ├── Tag.php │ └── User.php └── ObjectSerializer.php ``` The folder structure is simple and clear with nothing unexpected. Files are separated at the API level (pet, store, and user) and by model. There are a few helper files, like `ApiException.php`. Below is the output for Speakeasy. ```sh ├── Models │ ├── Components │ │ ├── ApiResponse.php │ │ ├── Category.php │ │ ├── Order1.php │ │ ├── Order2.php │ │ ├── Order3.php │ │ ├── Order4.php │ │ ├── Order5.php │ │ ├── Order6.php │ │ ├── OrderStatus.php │ │ ├── Pet1.php │ │ ├── Pet10.php │ │ ├── Pet11.php │ │ ├── Pet12.php │ │ ├── Pet13.php │ │ ├── Pet14.php │ │ ├── Pet15.php │ │ ├── Pet16.php │ │ ├── Pet17.php │ │ ├── Pet18.php │ │ ├── Pet19.php │ │ ├── Pet2.php │ │ ├── Pet20.php │ │ ├── Pet21.php │ │ ├── Pet22.php │ │ ├── Pet3.php │ │ ├── Pet4.php │ │ ├── Pet5.php │ │ ├── Pet6.php │ │ ├── Pet7.php │ │ ├── Pet8.php │ │ ├── Security.php │ │ ├── Status.php │ │ ├── Tag.php │ │ ├── User1.php │ │ ├── User10.php │ │ ├── User11.php │ │ ├── User12.php │ │ ├── User13.php │ │ ├── User15.php │ │ ├── User2.php │ │ ├── User3.php │ │ ├── User4.php │ │ ├── User5.php │ │ ├── User6.php │ │ ├── User7.php │ │ ├── User8.php │ │ └── User9.php │ ├── Errors │ │ └── SDKException.php │ └── Operations │ ├── AddPetFormResponse.php │ ├── AddPetJsonResponse.php │ ├── AddPetRawResponse.php │ ├── CreateUserFormResponse.php │ ├── CreateUserJsonResponse.php │ ├── CreateUserRawResponse.php │ ├── CreateUsersWithListInputResponse.php │ ├── DeleteOrderRequest.php │ ├── DeleteOrderResponse.php │ ├── DeletePetRequest.php │ ├── DeletePetResponse.php │ ├── DeleteUserRequest.php │ ├── DeleteUserResponse.php │ ├── FindPetsByStatusRequest.php │ ├── FindPetsByStatusResponse.php │ ├── FindPetsByTagsRequest.php │ ├── FindPetsByTagsResponse.php │ ├── GetInventoryResponse.php │ ├── GetInventorySecurity.php │ ├── GetOrderByIdRequest.php │ ├── GetOrderByIdResponse.php │ ├── GetPetByIdRequest.php │ ├── GetPetByIdResponse.php │ ├── GetPetByIdSecurity.php │ ├── GetUserByNameRequest.php │ ├── GetUserByNameResponse.php │ ├── LoginUserRequest.php │ ├── LoginUserResponse.php │ ├── LogoutUserResponse.php │ ├── PlaceOrderFormResponse.php │ ├── PlaceOrderJsonResponse.php │ ├── PlaceOrderRawResponse.php │ ├── Status.php │ ├── UpdatePetFormResponse.php │ ├── UpdatePetJsonResponse.php │ ├── UpdatePetRawResponse.php │ ├── UpdatePetWithFormRequest.php │ ├── UpdatePetWithFormResponse.php │ ├── UpdateUserFormRequest.php │ ├── UpdateUserFormResponse.php │ ├── UpdateUserJsonRequest.php │ ├── UpdateUserJsonResponse.php │ ├── UpdateUserRawRequest.php │ ├── UpdateUserRawResponse.php │ ├── UploadFileRequest.php │ └── UploadFileResponse.php ├── Pet.php ├── SDK.php ├── SDKBuilder.php ├── SDKConfiguration.php ├── Store.php ├── User.php └── Utils ├── DateHandler.php ├── DateTimeHandler.php ├── DefaultRequest.php ├── DefaultResponse.php ├── DefaultStream.php ├── DefaultUri.php ├── EnumHandler.php ├── FormMetadata.php ├── Headers.php ├── JSON.php ├── MixedJSONHandler.php ├── MultipartMetadata.php ├── ParamsMetadata.php ├── PathParameters.php ├── PhpDocTypeParser.php ├── QueryParameters.php ├── RequestBodies.php ├── RequestMetadata.php ├── Security.php ├── SecurityClient.php ├── SecurityMetadata.php ├── SpeakeasyMetadata.php ├── UnionHandler.php └── Utils.php ``` The Speakeasy SDK is more complex and has more features. Files are separated at a lower level than OpenAPI Generator — at the operation level – and further split into content types of the operation, like `AddPetJsonResponse.php`. There are more helper files bundled with the SDK in the `Utils` folder. ## Code readability We'll compare the SDKs in terms of code readability, focusing on the `Pet` model first. ### OpenAPI Generator The `Pet` model generated by OpenAPI Generator inherits a `ModelInterface` and has a `container` property that holds the model's fields. The model's constructor can either take an associative array of field names and values or no arguments. Then, the model exposes getter and setter methods for each field. Type mapping is presented as an associative array of field names and types as strings. The `Pet` model has the following fields: ```php filename="app/og/lib/Model/Pet.php" //... protected static $openAPITypes = [ 'id' => 'int', 'name' => 'string', 'category' => '\OpenAPI\Client\Model\Category', 'photo_urls' => 'string[]', 'tags' => '\OpenAPI\Client\Model\Tag[]', 'status' => 'string' ]; //... ``` Overall, the `Pet` model is extremely verbose, coming in at 623 lines of code, including comments and whitespace, but excluding dependencies. Contrast this with the `Pet` model generated by Speakeasy. ### Speakeasy The `Pet10` model generated by Speakeasy is more concise and readable, presented in its entirety below: ```php filename="app/se/src/Models/Components/Pet10.php" $photoUrls */ #[SpeakeasyMetadata('form:name=photoUrls')] public array $photoUrls; /** * $tags * * @var ?array $tags */ #[SpeakeasyMetadata('form:name=tags,json=true')] public ?array $tags = null; /** * pet status in the store * * @var ?Status $status */ #[SpeakeasyMetadata('form:name=status')] public ?Status $status = null; /** * @param string $name * @param array $photoUrls * @param ?int $id * @param ?Category $category * @param ?array $tags * @param ?Status $status */ public function __construct(string $name, array $photoUrls, ?int $id = null, ?Category $category = null, ?array $tags = null, ?Status $status = null) { $this->name = $name; $this->photoUrls = $photoUrls; $this->id = $id; $this->category = $category; $this->tags = $tags; $this->status = $status; } } ``` The `Pet10` model, at 76 lines of code, including comments and whitespace, is more concise and readable than the `Pet` model generated by OpenAPI Generator. Speakeasy uses modern PHP features like typed properties, attributes, and named arguments to make the model more readable. Serialization and deserialization are handled by [JMS/Serializer](http://jmsyst.com/libs/serializer), which uses annotations in the model to convert objects to and from JSON. This allows Speakeasy to create more concise and readable code. Instead of using a getter and setter for each field, Speakeasy uses typed properties and a constructor to set the fields. This makes implementing the model more straightforward and less verbose. ## Dependencies The OpenAPI Generator SDK Composer file has the dependencies below. - The ext-curl, ext-json, and ext-mbstring PHP extensions, which handle calling HTTP, serialize objects to JSON, and work with Unicode. - [Guzzle](https://docs.guzzlephp.org/en/stable) and [Guzzle PSR-7](https://github.com/guzzle/psr7) send HTTP requests with [PSR-7](https://www.php-fig.org/psr/psr-7/) support. - [PHPUnit](https://phpunit.de/documentation.html) runs tests. - [Symfony PHP Coding Standards Fixer](https://cs.symfony.com/) formats code. The Speakeasy SDK Composer file has the dependencies below. - [Guzzle](https://docs.guzzlephp.org/en/stable) sends HTTP requests. - [Serializer](https://jmsyst.com/libs/serializer) converts PHP objects to and from JSON and XML to be sent over HTTP. - [Brick\DateTime](https://github.com/brick/date-time) manages dates, times, and time zones. - [phpDocumentor TypeResolver](https://github.com/phpDocumentor/TypeResolver) generates types from DocBlocks. - [Laravel Pint](https://laravel.com/docs/11.x/pint) formats code. - [PHPStan](https://phpstan.org/) finds errors and handles complex types. - [PHPUnit](https://phpunit.de/documentation.html) runs tests. However, there are no tests in the created SDK. - [Rector](https://github.com/rectorphp/rector) checks code quality. - [Roave Security Advisories](https://github.com/Roave/SecurityAdvisories) warns about dangerous Composer dependencies. Both creators use similar libraries, but OpenAPI Generator relies as much as possible on core PHP extensions, while Speakeasy has more serialization and complex typing libraries: Serializer, Brick, TypeResolver, and PHPStan. ## Supported PHP versions At the time of compiling this comparison, the Speakeasy SDK required at least PHP version 8.1. PHP 8 introduced language features to support stronger typing. The OpenAPI Generator SDK still supports PHP version 7.4, though it is compatible with PHP 8. We recommend you use the latest PHP version with both SDKs. ## Strong typing Both creators use DocBlocks to provide type annotations to all parameters and variables in the SDKs, which is useful for IDEs and for programmers to understand the code. But files in the Speakeasy SDK include the line `declare(strict_types=1);`, which causes PHP to throw a `TypeError` if a function accepts or returns an invalid type at runtime. The OpenAPI Generator SDK files do not have this line and so don't check types at runtime. In Speakeasy, the JMS Serializer checks types when converting from JSON to PHP objects at runtime. OpenAPI Generator doesn't have this in plain Guzzle. ### Enums OpenAPI Generator provides a workaround for enumerations using constant strings and functions. Below is the pet status enumeration for OpenAPI Generator. ```php public const STATUS_AVAILABLE = 'available'; public const STATUS_PENDING = 'pending'; public const STATUS_SOLD = 'sold'; /** * Gets allowable values of the enum * * @return string[] */ public function getStatusAllowableValues() { return [ self::STATUS_AVAILABLE, self::STATUS_PENDING, self::STATUS_SOLD, ]; } ``` Below is the pet status enumeration for Speakeasy using modern PHP. ```php enum Status: string { case Available = 'available'; case Pending = 'pending'; case Sold = 'sold'; } ``` ### Content types Below are the content types in the schema for updating a pet, in JSON, XML, or as a form. ```yaml requestBody: content: application/json: schema: $ref: "#/components/schemas/Pet" application/xml: schema: $ref: "#/components/schemas/Pet" application/x-www-form-urlencoded: schema: $ref: "#/components/schemas/Pet" ``` Speakeasy supports JSON and form content types, but not XML. OpenAPI Generator supports all three. Additionally, OpenAPI Generator provides asynchronous versions of each HTTP call, such as `AddPet` and `AddPetAsync`. In Speakeasy, each content type for each operation will become its own file in the SDK. In OpenAPI Generator, all operations are combined into one API file. ### Unions In OpenAPI, you can use `oneOf` in a schema like this: ```yaml Pet: type: object properties: age: oneOf: - type: integer - type: string ``` The `age` property will be typed as a union in PHP in Speakeasy: ```php class Pet10 { /** * * @var int|string|null $age */ #[SpeakeasyMetadata('form:name=age')] public int|string|null $age = null; ... public function __construct(?string $name = null, ?array $photoUrls = null, int|string|null $age = null, ``` OpenAPI Generator can handle this schema, but creates a 380-line file called `PetAge.php` with custom code to implement unions. ## Created documentation Both Speakeasy and OpenAPI Generator create a `docs` directory with Markdown documentation and PHP usage examples for every operation and every model. We found the usage examples in the Speakeasy SDK worked flawlessly, while the examples in the OpenAPI Generator SDK don't always include required fields when instantiating objects. For instance, the `PetApi.md` example in the OpenAPI Generator SDK doesn't include any fields for the `Pet` object. ```php filename="app/og/docs/PetApi.md" setAccessToken('YOUR_ACCESS_TOKEN'); $apiInstance = new OpenAPI\Client\Api\PetApi( // If you want to use a custom http client, pass your client which implements `GuzzleHttp\ClientInterface`. // This is optional, `GuzzleHttp\Client` will be used as default. new GuzzleHttp\Client(), $config ); $pet = new \OpenAPI\Client\Model\Pet(); // \OpenAPI\Client\Model\Pet | Create a new pet in the store try { $result = $apiInstance->addPet($pet); print_r($result); } catch (Exception $e) { echo 'Exception when calling PetApi->addPet: ', $e->getMessage(), PHP_EOL; } ``` Both SDKs include detailed documentation for operations and models, but the Speakeasy SDK includes more detailed usage examples that work out of the box. Speakeasy also creates appropriate example strings based on a field's `format` in the OpenAPI schema. For example, if we add `format: uri` to the item for a pet's photo URLs, we can compare each SDK's usage documentation for this field. The SDK created by Speakeasy includes a helpful example of this field that lists multiple random URLs: ```php # Speakeasy SDK Usage Example pet = shared.Pet( # ... photo_urls=[ 'https://salty-stag.name', 'https://moral-star.info', 'https://present-giggle.info', ] ) ``` The OpenAPI Generator SDK's documentation uses a single random string in its example: ```php # PHP SDK Usage Example pet = Pet( # ... photo_urls=[ "photo_urls_example" ] ) ``` ## Automation This comparison focuses on installing and using Speakeasy and OpenAPI Generator using the command line, but both tools can also run as part of a CI workflow. For example, you can set up a [GitHub Action](https://github.com/speakeasy-api/sdk-generation-action) to ensure your Speakeasy SDK is always up-to-date when your API schema changes. ## Unsupported features At the time of writing, OpenAPI Generator does not support: - [Data types null, UUID](https://openapi-generator.tech/docs/generators/php/#data-type-feature), [all, any, and union](https://openapi-generator.tech/docs/generators/php/#schema-support-feature). - [Server URLs with parameters](https://openapi-generator.tech/docs/generators/php/#global-feature). - [Callbacks](https://openapi-generator.tech/docs/generators/php/#global-feature) (allowing your server to call a client). - [Link objects](https://openapi-generator.tech/docs/generators/php/#global-feature) (relating operations to each other to indicate a workflow). Neither service supports OAuth 2 flows other than Implicit. ## Summary Open-source tooling can be a great way to experiment, but if you're working on production code, the Speakeasy PHP SDK creator will help ensure that you create reliable and performant PHP SDKs. The Speakeasy PHP SDK creator uses strong typing to provide safe runtime performance, supports many OpenAPI features, and is rapidly adding more. # Python Feature Reference Source: https://speakeasy.com/docs/sdks/languages/python/feature-support import { Table } from "@/mdx/components"; ## Authentication
## Server Configuration
## Data Types ### Basic Types
### Polymorphism
## Methods
## Parameters
### Path Parameters Serialization
### Query Parameters Serialization
## Requests
## Responses
## Documentation
# Generate Python SDKs from OpenAPI / Swagger Source: https://speakeasy.com/docs/sdks/languages/python/methodology-python import { Callout } from "@/mdx/components"; import { FileTree } from "nextra/components"; For a comparison between the Speakeasy Python SDK and some popular open-source generators, see [**this page**](/post/speakeasy-oss-python-generator). ## SDK Overview Speakeasy-generated Python SDKs are designed to be best in class, providing a seamless developer experience and full type safety, alongside asynchronous support. The core Python SDK features include: - Fully type-annotated classes and methods with full Pydantic models and associated TypedDicts. - Async and Sync methods for all endpoints. - Support for streaming uploads and downloads. - Support for Server-Sent Events (SSE) with automatic method overloads for enhanced type safety. - Authentication support for OAuth flows and support for standard security mechanisms (HTTP Basic, application tokens, etc.). - Optional pagination support for supported APIs. - Optional support for retries in every operation. - Complex number types including big integers and decimals. - Date and date/time types using RFC3339 date formats. - Custom type enums using strings and integers (including Open Enums). - Union types and combined types. ### Python Package Structure By default, `uv` handles Python dependencies and packaging, but users can configure `poetry` instead via the `packageManager` option. ## Python Type Safety Modern Python uses type hints to improve code readability and so do Speakeasy-generated Python SDKs! Speakeasy-generated Python SDKs expose type annotations for developers to perform type checks at runtime and increase type safety, we also employ Pydantic models to ensure that the data passed to and from the SDK is valid at runtime. ### The generated models Speakeasy uses `pydantic` for all generated models to correctly serialize and deserialize objects; whether the objects are passed as query parameters, path parameters, or request bodies. Metadata based on the definitions provided by the OpenAPI document are appended to fields. For example, this is the generated class for the [Drink](https://github.com/speakeasy-sdks/template-sdk/blob/main/openapi.yaml#L312) component from our [SpeakeasyBar template repository](https://github.com/speakeasy-sdks/template-sdk): ```python class Drink(BaseModel): name: str r"""The name of the drink.""" price: float r"""The price of one unit of the drink in US cents.""" type: Optional[DrinkType] = None r"""The type of drink.""" stock: Optional[int] = None r"""The number of units of the drink in stock, only available when authenticated.""" product_code: Annotated[Optional[str], pydantic.Field(alias="productCode")] = None r"""The product code of the drink, only available when authenticated.""" ``` Python also generates matching `TypedDict` classes for each model, which can be used to pass in dictionaries to the SDK methods without the need to import the model classes. ```python class DrinkTypedDict(TypedDict): name: str r"""The name of the drink.""" price: float r"""The price of one unit of the drink in US cents.""" type: NotRequired[DrinkType] r"""The type of drink.""" stock: NotRequired[int] r"""The number of units of the drink in stock, only available when authenticated.""" product_code: NotRequired[str] r"""The product code of the drink, only available when authenticated.""" ``` which allows methods to be called one of two ways: ```python res = s.orders.create_order(drinks=[ { "type": bar.OrderType.INGREDIENT, "product_code": "AC-A2DF3", "quantity": 138554, }, ]) ``` or ```python res = s.orders.create_order(drinks=[ Drink( type=bar.OrderType.INGREDIENT, product_code="AC-A2DF3", quantity=138554, ), ]) ``` ## Server-Sent Events (SSE) with type safety Python SDKs automatically generate method overloads for Server-Sent Events operations when the `inferSSEOverload` configuration is enabled (default: `true`). This feature provides enhanced type safety by creating separate method signatures for streaming and non-streaming responses. When an operation meets the following criteria, Speakeasy will generate overloaded methods: - The operation has a required request body - The request body contains a `stream` field (boolean type) - The operation has exactly two responses: one `text/event-stream` and one `application/json` ```python # Non-streaming method - returns JSON response object response = s.chat.create(prompt="Hello world", stream=False) print(response.content) # Fully typed ChatResponse # Streaming method - returns SSE event iterator stream = s.chat.create(prompt="Hello world", stream=True) for event in stream: print(event.data) # Fully typed ChatEvent ``` This eliminates the need for runtime type checking and provides better IDE support with accurate type hints for both streaming and non-streaming use cases. ## Async vs Sync Methods Speakeasy-generated Python SDKs provide both synchronous and asynchronous methods for all endpoints. The SDK uses the `httpx` library for making HTTP requests, which supports both synchronous and asynchronous requests. Synchronous: ```python res = s.orders.create_order(drinks=[ Drink( type=bar.OrderType.INGREDIENT, product_code="AC-A2DF3", quantity=138554, ), ]) ``` Asynchronous: ```python res = await s.orders.create_order_async(drinks=[ Drink( type=bar.OrderType.INGREDIENT, product_code="AC-A2DF3", quantity=138554, ), ]) ``` Python SDKs can also be configured to use a constructor-based async pattern with separate `AsyncMyAPI` classes. See the [asyncMode configuration option](/docs/speakeasy-reference/generation/python-config#async-method-configuration) for details. ## HTTP Client To make API calls, the Python SDK instantiates its own HTTP client using the `Client` class from the `httpx` library. This allows authentication settings to persist across requests and reduce overhead. ## Parameters If configured, Speakeasy will generate methods with parameters for each parameter defined in the OpenAPI document, as long as the number of parameters is less than or equal to the configured `maxMethodParams` value in the `gen.yaml` file. If the number of parameters exceeds the configured `maxMethodParams` value or is set to `0`, a request object will be generated for the method to pass in all parameters as a single object. ## Errors The Python SDK will raise exceptions for any network or invalid request errors. For unsuccessful responses, if a custom error response is specified in the spec file, the SDK will unmarshal the HTTP response details into the custom error response to be thrown as an exception. When no custom response is specified in the spec, the SDK will throw an `SDKException` with details of the failed response. ```python import sdk from sdk.models import errors s = sdk.SDK() res = None try: res = s.errors.status_get_x_speakeasy_errors(status_code=385913) except errors.StatusGetXSpeakeasyErrorsResponseBody as e: # handle exception except errors.SDKError as e: # handle exception if res is not None: # handle response pass ``` ## User Agent Strings The Python SDK includes a [user agent](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/User-Agent) string in all requests. This can be leveraged to track SDK usage amongst broader API usage. The format is as follows: ```stmpl speakeasy-sdk/python {{SDKVersion}} {{GenVersion}} {{DocVersion}} {{PackageName}} ``` Where - `SDKVersion` is the version of the SDK, defined in `gen.yaml` and released - `GenVersion` is the version of the Speakeasy generator - `DocVersion` is the version of the OpenAPI document - `PackageName` is the name of the package defined in `gen.yaml` ## SDK Memory footprint and Load time Speakeasy-generated Python SDKs employ a lazy loading mechanism for their functional components (e.g., groups of operations like `orders`, `users`, etc.) and their associated models. This means these components are not loaded into memory when you initially import the SDK or create an SDK instance. Instead, a specific component is loaded only the first time you access it. **Example:** ```python import sdk # Initial SDK instantiation: # Only the core SDK logic is loaded. # Modules for specific API features like 'orders' are NOT yet loaded. s = sdk.SDK() # First access to a component: # When s.orders is accessed here for the first time, # the 'orders' module is dynamically imported and initialized. # This is the point where the code for 'orders' functionality is loaded. orders_list = s.orders.list_orders() # Subsequent accesses: # The 'orders' component is now loaded and cached. # Further operations on s.orders use the already-loaded component directly. order_details = s.orders.get_details(id="some_id") ``` This design provides two key benefits: - **Reduced Memory Footprint:** Your application only loads the parts of the SDK it actually uses, conserving memory. - **Faster Startup Times:** The initial `import` of the SDK and its instantiation are quicker, as it avoids pre-emptively loading all possible components. Collectively, these improvements contribute to a smoother and more efficient developer experience. # Comparison guide: OpenAPI/Swagger Python client generation Source: https://speakeasy.com/docs/sdks/languages/python/oss-comparison-python import { Callout, Table } from "@/mdx/components"; Many of our users have switched from [OpenAPI Generator](https://openapi-generator.tech/) to Speakeasy for their Python SDKs. Learn how to use both SDK creators in this guide, and the differences between them. Open-source OpenAPI generators are great for experimentation but lack the reliability, performance, and intuitive developer experience required for critical applications. As an alternative, Speakeasy creates [idiomatic SDKs](/post/client-sdks-as-a-service) that meet the bar for enterprise use. In this post, we'll focus on Python, but Speakeasy can also create SDKs in Go, TypeScript, Java, Ruby, PHP, and more. - [Read about the differences between TypeScript SDKs generated by Speakeasy and OpenAPI Generator.](/post/speakeasy-sdk-vs-openapi-typescript-generator/)
- [Read about the differences between Go SDKs generated by Speakeasy and OpenAPI Generator.](/post/speakeasy-oss-go-generator)
Here's a summary of the major differences between a Python SDK created using Speakeasy, compared to an SDK created by the OpenAPI Generator. Unless support for Python 3.7 is critically important, Speakeasy is recommended for Python SDKs.
For the full technical walkthrough, read on! ## What is OpenAPI Generator? OpenAPI Generator, as opposed to an OpenAPI generator, is a specific community-run open-source SDK generator for the OpenAPI specification, [focusing on version 3](https://openapi-generator.tech/docs/fork-qna). The [Swagger Codegen](https://swagger.io/tools/swagger-codegen) tool is similar, but run by the company Smartbear. OpenAPI Generator was forked from Swagger Codegen. ## Preparing the SDK generators For our comparison, we ran Speakeasy and OpenAPI Generator each in its own Docker container, which works on Windows, macOS, or Linux. Using Docker instead of running code directly on your physical machine is safer, as the code cannot access files outside the folder you specify. We used the PetStore 3.1 YAML schema file from the [Swagger editor](https://editor-next.swagger.io) examples menu in **File > Load Example > OpenAPI 3.1 Petstore**. To follow along with this guide, save the file to a subfolder called `app` in your current path, such as `app/petstore31.yaml`. We changed the schema to use the server version 3.1: ```yaml servers: - url: https://petstore31.swagger.io/api/v31 ``` OpenAPI Generator provides a Docker image, but Speakeasy does not. To install the Speakeasy CLI, you can either install the Go binary directly on your computer following the steps in the [Speakeasy Getting Started](/docs/speakeasy-reference/cli/getting-started) guide, or run it in Docker, which we did. To use Docker yourself, first create a Docker file called `se.dockerfile` with the content below, replacing `YourApiKey` with your key from the Speakeasy website. ```bash FROM alpine:3.19 WORKDIR /app RUN apk add bash go curl unzip sudo nodejs npm RUN curl -fsSL https://go.speakeasy.com/cli-install.sh | sh; ENV GOPATH=/root/go ENV PATH=$PATH:$GOPATH/bin ENV SPEAKEASY_API_KEY=YourApiKey ``` Then build the Speakeasy image with the command below. ```sh docker build -f se.dockerfile -t seimage . ``` ## Validating the schemas Both OpenAPI Generator and the Speakeasy CLI can validate an OpenAPI schema. We'll run both and compare the output. ### Validation using OpenAPI Generator To validate `petstore31.yaml` using OpenAPI Generator, run the following in the terminal: ```bash docker run --rm -v "./app:/local" openapitools/openapi-generator-cli validate -i /local/petstore31.yaml ``` OpenAPI Generator returns two warnings: ``` Warnings: - Unused model: Address - Unused model: Customer [info] Spec has 2 recommendation(s). ``` ### Validation using Speakeasy Validate the schema with Speakeasy by running the following in the terminal: ```bash docker run --rm -v "./app:/app" seimage speakeasy validate openapi -s /app/petstore31.yaml ``` The Speakeasy validator returns 72 hints about missing examples, seven warnings about missing responses, and three warnings about unused components. Each warning includes a detailed JSON-formatted error with line numbers. Since both validators returned only warnings and not errors, we can assume both generators will create SDKs without issues. ## Creating the SDKs First, we'll generate an SDK with OpenAPI Generator, and then we'll create one with Speakeasy. ### Generating an SDK with OpenAPI Generator OpenAPI Generator includes two different Python SDK generators (and four server generators): * [python](https://openapi-generator.tech/docs/generators/python) * [python-pydantic-v1](https://openapi-generator.tech/docs/generators/python-pydantic-v1) The only difference between the two is that `python` is the latest version, which uses Pydantic V2. You can ignore Pydantic V1. To generate an SDK from the schema file in OpenAPI Generator, we ran the command given in the [OpenAPI Generator readme](https://github.com/OpenAPITools/openapi-generator#16---docker) below. ```sh docker run --rm -v "./app:/local" openapitools/openapi-generator-cli generate -i /local/petstore31.yaml -g python -o /local/og --additional-properties=packageName=petstore_sdk,projectName=petstore-sdk-python ``` This command gives one warning that looks like it could cause errors, `o.o.codegen.utils.ModelUtils - Failed to get the schema name: null`, but OpenAPI Generator successfully created three folders:
The generator warned us in the terminal that `Generation using 3.1.0 specs is in development and is not officially supported yet.` The OpenAPI Generator [roadmap](https://openapi-generator.tech/docs/roadmap) hasn't been updated in almost two years. ### Creating an SDK with Speakeasy Next, we'll create an SDK using the Speakeasy CLI with the command below. ```bash docker run --rm -v "./app:/app" seimage speakeasy quickstart ``` Speakeasy creates the following folders.
Unlike the OpenAPI Generator output, the Speakeasy output includes no tests. ## SDK code comparison: Package structure We'll start our comparison by looking at the structure of each SDK: the OpenAPI Generator SDK and the Speakeasy SDK. To count the lines of code in the SDKs, we'll run `cloc` for each (ignoring documentation and test folders): ```bash cloc ./app/og/petstore_sdk cloc ./app/se/src/openapi ``` Below are the results for each SDK.
We see that the Speakeasy SDK has about the same number of lines of code and lines of comments as OpenAPI Generator, but OpenAPI Generator leaves more blank lines. The following commands output the files of each SDK. ```sh tree ./app/og/petstore_sdk # exclude docs and tests tree ./app/se/src/openapi ``` Below is the output for OpenAPI Generator. ```sh ├── api │ ├── __init__.py │ ├── pet_api.py │ ├── store_api.py │ └── user_api.py ├── api_client.py ├── api_response.py ├── configuration.py ├── exceptions.py ├── __init__.py ├── models │ ├── address.py │ ├── api_response.py │ ├── category.py │ ├── customer.py │ ├── __init__.py │ ├── order.py │ ├── pet.py │ ├── tag.py │ └── user.py ├── py.typed └── rest.py ``` The folder structure is simple and clear with nothing unexpected. Files are separated at the API level (pet, store, and user) and by model. There are a few helper files, like `exceptions.py`. Below is the output for Speakeasy. ```sh ├── basesdk.py ├── _hooks │ ├── __init__.py │ ├── registration.py │ ├── sdkhooks.py │ └── types.py ├── httpclient.py ├── __init__.py ├── models │ ├── components │ │ ├── apiresponse.py │ │ ├── category.py │ │ ├── httpmetadata.py │ │ ├── __init__.py │ │ ├── order.py │ │ ├── pet.py │ │ ├── security.py │ │ ├── tag.py │ │ └── user.py │ ├── errors │ │ ├── __init__.py │ │ └── sdkerror.py │ ├── __init__.py │ └── operations │ ├── addpet_form.py │ ├── addpet_json.py │ ├── addpet_raw.py │ ├── createuser_form.py │ ├── createuser_json.py │ ├── createuser_raw.py │ ├── createuserswithlistinput.py │ ├── deleteorder.py │ ├── deletepet.py │ ├── deleteuser.py │ ├── findpetsbystatus.py │ ├── findpetsbytags.py │ ├── getinventory.py │ ├── getorderbyid.py │ ├── getpetbyid.py │ ├── getuserbyname.py │ ├── __init__.py │ ├── loginuser.py │ ├── logoutuser.py │ ├── placeorder_form.py │ ├── placeorder_json.py │ ├── placeorder_raw.py │ ├── updatepet_form.py │ ├── updatepet_json.py │ ├── updatepet_raw.py │ ├── updatepetwithform.py │ ├── updateuser_form.py │ ├── updateuser_json.py │ ├── updateuser_raw.py │ └── uploadfile.py ├── pet.py ├── sdkconfiguration.py ├── sdk.py ├── store.py ├── types │ ├── basemodel.py │ └── __init__.py ├── user.py └── utils ├── enums.py ├── eventstreaming.py ├── forms.py ├── headers.py ├── __init__.py ├── metadata.py ├── queryparams.py ├── requestbodies.py ├── retries.py ├── security.py ├── serializers.py ├── url.py └── values.py ``` The Speakeasy SDK is more complex and has more features. Files are separated at a lower level than OpenAPI Generator — at the operation level - and further split into media types of the operation, like `addpet_json.py`. There are more helper files bundled with the SDK in the `utils` folder. The `_hooks` folder allows you to [insert custom code](/docs/customize/code/sdk-hooks) into the SDK. ## Calling the server Swagger provides a complete test server for the PetStore OpenAPI version 3.1 server at https://petstore31.swagger.io. (There is a version 3.0 server too, but that gives 500 errors when called.) To check that both SDKs contain code that actually works, we called the pet operations given in the readme files against the test server. We used a Docker Python 3.8 container, as 3.8 is supported by both OpenAPI Generator and Speakeasy. ### Calling the server with OpenAPI Generator For OpenAPI Generator, we ran the command below, with a successful response. First, the command installs the Python dependencies in the Docker container as recommended in the SDK `README.md` file, then it runs the sample `main.py` script to call the server using the SDK. ```sh docker run --rm -v "./app/og:/app" -w "/app" python:3.8.19-alpine3.20 sh -c "pip install . && python setup.py install && python main.py" # The response of PetApi->add_pet: # Pet(id=10, name='doggie', category=None, photo_urls=['string'], tags=[], status='available') ``` The `README.md` does not give clear instructions for installing all dependencies. After running the installation commands above, `pytest` was not installed, even though it was mentioned in `README.md`. Below is the `main.py` script to call the API. ```py import petstore_sdk from petstore_sdk.models.pet import Pet from petstore_sdk.rest import ApiException from pprint import pprint configuration = petstore_sdk.Configuration( host = "https://petstore31.swagger.io/api/v31" ) with petstore_sdk.ApiClient(configuration) as api_client: api_instance = petstore_sdk.PetApi(api_client) pet = Pet.from_json('''{ "id": 10, "name": "doggie", "photoUrls": [ "string" ], "status": "available" }''') try: api_response = api_instance.add_pet(pet) print("The response of PetApi->add_pet:\n") pprint(api_response) except ApiException as e: print("Exception when calling PetApi->add_pet: %s\n" % e) ``` The example code was partially given in `README.md`, but we needed to add the pet JSON sample from https://petstore31.swagger.io/#/pet/addPet. ### Calling the server with Speakeasy Speakeasy also called the server successfully. The command below is almost identical to the one for OpenAPI Generator in the previous section, except that the Speakeasy SDK has more dependencies than OpenAPI Generator. Poetry needs packages that are not included with a minimal Linux installation. ```sh docker run --rm -v "./app/se:/app" -w "/app" python:3.8.19-alpine3.20 sh -c "apk update && apk add --no-cache gcc musl-dev libffi-dev openssl-dev python3-dev py3-cffi py3-cryptography make && pip install --upgrade pip && pip install . && pip install poetry && poetry install && python main.py" # Updated pet name: doggie ``` Before `poetry install` would work however, we had to comment out the invalid line in `pyproject.toml`: ```toml [tool.poetry.group.extraDependencies.dependencies] # dev = "[object Object]" ``` Below is the `main.py` script to call the API. The code comes straight from the `README.md` file (except the print line), including the correct JSON to create a pet. ```py from openapi import SDK s = SDK(petstore_auth="",) res = s.pet.update_pet_json(request={ "name": "doggie", "photo_urls": [ "", ], "id": 10, "category": { "id": 1, "name": "Dogs", }, }) if res.pet is not None: print("Updated Pet Name:", res.pet.name) ``` ## Models, data containers, and typing Both SDKs use strong typing in their models. The OpenAPI Generator and Speakeasy SDK models inherit from the Pydantic `BaseModel` class. [Pydantic](https://docs.pydantic.dev/latest) validates data at runtime and provides clear error messages when data is invalid. For example, creating a `Pet` object with the `name` field set to an integer value will result in a validation error: ```python # Python Nextgen SDK import petstore_sdk pet = petstore_sdk.Pet(id=1, name="Archie", photoUrls=[]) pet2 = petstore_sdk.Pet(id=2, name=2, photoUrls=[]) # > pydantic.error_wrappers.ValidationError: 1 validation error for Pet # > name # > str type expected (type=type_error.str) ``` Both SDKs create a `BaseModel` pet like below. ```py class Pet(BaseModel): name: Annotated[str, FieldMetadata(form=True)] photo_urls: Annotated[List[str], pydantic.Field(alias="photoUrls"), FieldMetadata(form=True)] id: Annotated[Optional[int], FieldMetadata(form=True)] = None category: Annotated[Optional[Category], FieldMetadata(form=FormMetadata(json=True))] = None tags: Annotated[Optional[List[Tag]], FieldMetadata(form=FormMetadata(json=True))] = None status: Annotated[Annotated[Optional[Status], PlainValidator(validate_open_enum(False))], FieldMetadata(form=True)] = None r"""pet status in the store""" ``` In addition, Speakeasy adds `TypedDict`s to its components. An example is from `Pet.py` below. ```py class PetTypedDict(TypedDict): name: str photo_urls: List[str] id: NotRequired[int] category: NotRequired[CategoryTypedDict] tags: NotRequired[List[TagTypedDict]] status: NotRequired[Status] r"""pet status in the store""" ``` These typings provide strong runtime type checking and static type safety, which your IDE can use, too. Speakeasy also has enums in models, which OpenAPI Generator does not. Below is an example from the pet model. ```python class Status(str, Enum): r"""pet status in the store""" AVAILABLE = 'available' PENDING = 'pending' SOLD = 'sold' ``` Contrast this enum with the string-based specification and validation created by OpenAPI Generator: ```python class Pet(BaseModel): id: Optional[StrictInt] = None ... status: Optional[StrictStr] = Field(default=None, description="pet status in the store") ... @field_validator('status') def status_validate_enum(cls, value): """Validates the enum""" if value is None: return value if value not in set(['available', 'pending', 'sold']): raise ValueError("must be one of enum values ('available', 'pending', 'sold')") return value def to_str(self) -> str: """Returns the string representation of the model using alias""" return pprint.pformat(self.model_dump(by_alias=True)) def to_json(self) -> str: """Returns the JSON representation of the model using alias""" # TODO: pydantic v2: use .model_dump_json(by_alias=True, exclude_unset=True) instead return json.dumps(self.to_dict()) ... many more boilerplate methods below... ``` The OpenAPI Generator class also has to include many boilerplate methods for Pydantic, which is done for every model in the schema. Speakeasy models are more concise. ### Open enums Speakeasy allows adding the custom attribute `x-speakeasy-unknown-values` to an OpenAPI field to allow [open enums](/post/open-enums). ```yaml status: type: string x-speakeasy-unknown-values: allow description: pet status in the store enum: - available - pending - sold ``` The SDK code for an open enum doesn't change much from a standard enum. It's shown below. ```py class Status(str, Enum, metaclass=utils.OpenEnumMeta): r"""pet status in the store""" AVAILABLE = 'available' PENDING = 'pending' SOLD = 'sold' ``` Adding `x-speakeasy-unknown-values` will result in a Python SDK that allows values for `status` that are outside the given list of `available`, `pending`, and `sold`. There will no longer be a conflict between a new version of an API sending an unknown enum value to an older version of an SDK. The disadvantage of this open enum technique is that it allows clients to send mistaken and incorrect values to the server. The OpenAPI Specification has no way to handle enum conflicts between schema versions currently, and so OpenAPI Generator has no similar feature to open enums. A standard solution is still being [debated on GitHub](https://github.com/OAI/OpenAPI-Specification/issues/1552). ## SDK dependencies OpenAPI Generator and Speakeasy use almost identical Python packages. OpenAPI Generator has the following dependencies in its `requirements.txt` file. ```text python_dateutil >= 2.5.3 setuptools >= 21.0.0 urllib3 >= 1.25.3, < 2.1.0 pydantic >= 2 typing-extensions >= 4.7.1 ``` OpenAPI Generator also has a `pyproject.toml` file, though OpenAPI Generator does not mention this file in the installation instructions. Speakeasy has the following dependencies in its `pyproject.toml` file. ```py httpx = "^0.27.0" jsonpath-python = "^1.0.6" pydantic = "^2.7.1" python-dateutil = "^2.9.0.post0" typing-inspect = "^0.9.0" ``` ### HTTP client library The OpenAPI Generator SDK uses `urllib3` in its HTTP clients, while the Speakeasy SDK uses the `urllib3` and the [HTTPX](https://www.python-httpx.org/) library. HTTPX is future-proofed for HTTP/2 and asynchronous method calls. As per the Speakeasy SDK readme, you can call the server using synchronous or asynchronous calls, as shown below. ```py import asyncio from openapi import SDK async def main(): res = await s.pet.update_pet_form_async(request={ "name": "doggie", ... ``` ## Supported Python versions At the time of compiling this comparison, the Speakeasy SDK required at least Python version 3.8, which is supported until October 2024. The OpenAPI Generator SDK required at least Python version 3.7, which ended support halfway through 2023. We recommend you use the latest Python version with both SDKs. ## Retries The SDK created by Speakeasy can automatically retry failed network requests or retry requests based on specific error responses. This provides a simpler developer experience for error handling. To enable this feature, use the Speakeasy `x-speakeasy-retries` extension in your schema. We'll update the PetStore schema to add retries to the `addPet` operation as a test. Edit `petstore31.yaml` and add the following to the `addPet` operation: ```yaml x-speakeasy-retries: strategy: backoff backoff: initialInterval: 500 # 500 milliseconds maxInterval: 60000 # 60 seconds maxElapsedTime: 3600000 # 5 minutes exponent: 1.5 ``` Add this snippet to the operation: ```yaml #... paths: /pet: # ... post: #... operationId: addPet x-speakeasy-retries: strategy: backoff backoff: initialInterval: 500 # 500 milliseconds maxInterval: 60000 # 60 seconds maxElapsedTime: 3600000 # 5 minutes exponent: 1.5 ``` Now we can rerun the Speakeasy creator to enable retries for failed network requests when creating a new pet. It is also possible to enable retries for the SDK as a whole by adding global `x-speakeasy-retries` at the root of the schema. ## Created documentation Both Speakeasy and OpenAPI Generator create a `docs` directory with documentation and usage examples. We found the usage examples in the Speakeasy SDK worked flawlessly, while the examples in the OpenAPI Generator SDK don't always include required fields when instantiating objects. For instance, the `Pet.md` example has the code below. ```py # TODO update the JSON string below json = "{}" ``` The OpenAPI Generator SDK's documentation is especially thorough regarding models and has a table of fields and their types for each model. The Speakeasy SDK's documentation is focused on usage, with a usage example for each operation for each model. Speakeasy also creates appropriate example strings based on a field's `format` in the OpenAPI schema. For example, if we add `format: uri` to the item for a pet's photo URLs, we can compare each SDK's usage documentation for this field. The SDK created by Speakeasy includes a helpful example of this field that lists multiple random URLs: ```python # Speakeasy SDK Usage Example pet = shared.Pet( # ... photo_urls=[ 'https://salty-stag.name', 'https://moral-star.info', 'https://present-giggle.info', ] ) ``` The OpenAPI Generator SDK's documentation uses a single random string in its example: ```python # Python SDK Usage Example pet = Pet( # ... photo_urls=[ "photo_urls_example" ] ) ``` ## Automation This comparison focuses on installing and using Speakeasy and OpenAPI Generator using the command line, but both tools can also run as part of a CI workflow. For example, you can set up a [GitHub Action](https://github.com/speakeasy-api/sdk-generation-action) to ensure your Speakeasy SDK is always up to date when your API schema changes. ## Unsupported features At the time of writing, OpenAPI Generator does not support: - [Data types null, any, union, and UUID](https://openapi-generator.tech/docs/generators/python/#data-type-feature). - [OAuth and OpenID security](https://openapi-generator.tech/docs/generators/python/#security-feature). - [Multiple servers](https://openapi-generator.tech/docs/generators/python/#global-feature) (the place where clients call your SDK) and server URLs with parameters. - [Callbacks](https://openapi-generator.tech/docs/generators/python/#global-feature) (allowing your server to call a client). - [Link objects](https://openapi-generator.tech/docs/generators/python/#global-feature) (relating operations to each other to indicate a workflow). Speakeasy supports all the features above. Nullable fields in a Speakeasy SDK are marked as `Optional[]`. Neither Speakeasy nor OpenAPI Generator supports XML requests and responses. ## Summary Open-source tooling can be a great way to experiment, but if you're working on production code, the Speakeasy Python SDK creator will help ensure that you create reliable and performant Python SDKs. The Speakeasy Python SDK creator uses data classes to provide good runtime performance and exceptional readability, and automatic retries for network issues make error handling straightforward. The Speakeasy-created documentation includes a working usage example for each operation. Finally, unlike other Python SDK creators, Speakeasy can publish your created SDK to PyPI and run it as part of your CI workflows. # Create Ruby SDKs from OpenAPI documents Source: https://speakeasy.com/docs/sdks/languages/ruby/methodology-ruby import { FileTree } from "nextra/components"; ## Ruby SDK overview The Speakeasy Ruby SDK is designed to be easy to use and debug, and uses modern, object-oriented programming in ruby for a robust and strongly-typed experience. Some core features of the SDK are: - Our custom Crystalline serialization/deserialization library, using the object oriented structure of your api components to provide a totally ruby-native object oriented interface to your api - Configure your api interactions at the SDK level or per operation, using simple class constructors and function parameters - Initial support for security. This includes complete support for simple security patterns, and built in support for some common OAuth flows and other standard security mechanisms. - SDK Hooks to allow custom behaviour at various stages of the request response cycle. ## Flexible Type Safety Type safety in ruby is a controversial topics, with no typing approach being universally accepted by the community. Therefore instead of making an executive decision, you can specify your preferred `typingStrat` in your `gen.yaml` file. ### No Type Safety If you want Speakeasy to generate an SDK that does not include any typing directives, specifying a `typingStrat` of 'none'. Please note that even in this mode, the Crystalline serialization/deserialization library will necessarily do some type checking as it's processing requests and responses. ### Sorbet Mode If, however, you prefer for Speakeasy to generate an SDK that includes thorough type checking, specify a `typingStrat` of `sorbet`. In this mode, all methods and classes, etc will be annotated with the corresponding `sig` directives. In addition, `sorbet` will be added as a dependency for your project, and at compilation, a full static typecheck will be run. ## External libraries The Speakeasy ruby SDK seeks to support the majority of the OpenAPI Specification features, and as such, supports some features that aren't contained in the ruby standard library. Speakeasy fills the gaps using some external dependencies, which are detailed below. ### HTTP For rich support of HTTP processing, retries, form handling, etc. we take advantage of the excellent [Faraday](https://lostisland.github.io/faraday/#/) library, along with two of it's companion libraries [Faraday Retry](https://github.com/lostisland/faraday-retry) and [Faraday Multipart](https://github.com/lostisland/faraday-multipart). ### Base64 Since Ruby 3, the stdlib no longer includes base64 functionality, so we require this related package. ### Linting With Rubocop We include rubocop as a development dependency, and ensure that all of our builds pass a lot of the linter checks. ### Tests [Mini Test](https://rubygems.org/gems/minitest/versions/5.25.5?locale=en) is included with the SDK for running tests. However, no tests are created for the SDK automatically. ## Ruby SDK package structure ## Ruby SDK data types and classes The Speakeasy Ruby SDK uses native types wherever possible: - `String` - `Date` - `DateTime` - `Integer` - `Float` - `TrueClass` & `FalseClass` SDK Components are generated as ruby classes, and use a simple DSL to publicly declare the fields and types of fields as found in the sdk. ## Parameters When configured, Speakeasy will include up to a specified number of parameters directly in the function signatures, rather than providing the list of parameters as an object to be passed to the operation methods. The maximum number of parameters to be placed in the method signature is set in the `maxMethodParams` option in the `gen.yaml` file. If `maxMethodParams` is not set or is set to `0`, no method parameters will be added. ## Errors The Speakeasy ruby SDK raises exceptions using the appropriate `error` class as defined in the sdk specification. Wrap requests in a `begin` block to handle the error in the response. ## User agent strings The ruby SDK includes a [user agent](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/User-Agent) string in all requests, which can be leveraged to track SDK usage amongst broader API usage. The format is as follows: ```stmpl speakeasy-sdk/ruby {{SDKVersion}} {{GenVersion}} {{DocVersion}} {{PackageName}} ``` - `SDKVersion` is the version of the SDK defined in `gen.yaml` and released. - `GenVersion` is the version of the Speakeasy generator. - `DocVersion` is the version of the OpenAPI document. - `PackageName` is the name of the package defined in `gen.yaml`. ## Feature examples Let's take a look at how OpenAPI features are mapped to ruby code. We'll use snippets from the [Swagger PetStore 3.1](https://petstore31.swagger.io/) OpenAPI document, [`openapi.yaml`](https://petstore31.swagger.io/api/v31/openapi.yaml). If you're not familiar with the example, it provides operations for managing users, customers, pets, and orders for pets in a hypothetical pet store. ### Tags Each `tag` in the OpenAPI document becomes a sub-sdk, captured in it's own file, containing all tagged operations, such as `pet.rb`, `store.rb`, and `user.rb` for: ```yaml tags: - name: pet description: Everything about your Pets externalDocs: description: Find out more url: http://swagger.io ... ``` ### Security The Swagger Petstore OpenAPI document uses API key security and OAuth 2.0: ```yaml /pet/{petId}: security: - api_key: [] - petstore_auth: - write:pets - read:pets ... components: securitySchemes: petstore_auth: type: oauth2 flows: implicit: authorizationUrl: https://petstore31.swagger.io/oauth/authorize scopes: write:pets: modify pets in your account read:pets: read your pets api_key: type: apiKey name: api_key in: header ``` The ruby SDK creates a security class you can call with either scheme: ```ruby class GetPetByIdSecurity include Crystalline::MetadataFields field :api_key, Crystalline::Nilable.new(::String), {'security': {'scheme': true, 'type': 'apiKey', 'subtype': 'header', 'name': 'api_key'}} field :pet_store_auth, Crystalline::Nilable.new(::String), {'security': {'scheme': true,'type': 'oauth2','name': 'Authorization'} } def initialize(api_key: nil, pet_store_auth: nil) @api_key = api_key @pet_store_auth = pet_store_auth end end # Example call: request_security = GetPetByIdSecurity.new(api_key: ''); response = sdk.pet.get_pet_by_id(request_security, 504151); ``` The implicit flow is the only OAuth flow currently supported. ### Enums Speakeasy uses sorbet `T::Enum` enums, if sorbet is the typing strategy: ```ruby class Status < T::Enum enums do AVAILABLE = new('available') PENDING = new('pending') SOLD = new('sold') end end ``` If you are not using sorbet, Crystalline provides a suitable mixin: ```ruby class Status include ::Crystalline::Enum enums do AVAILABLE = new('available') PENDING = new('pending') SOLD = new('sold') end end ``` ### Typed parameters Consider the following example of an array of strings in `openapi.yaml`: ```yaml /pet/findByTags: get: operationId: findPetsByTags parameters: - name: tags in: query required: false explode: true schema: type: array items: type: string ``` The ruby SDK provides sorbet types in a signature block. ```ruby sig { params(tags: T.nilable(T::Array[::String])).returns(FindPetsByTagsResponse) } def find_pets_by_tags(tags: nil) ``` You can use `oneOf` in an OpenAPI document like this: ```yaml Pet: type: object properties: age: oneOf: - type: integer - type: string ``` The `age` property will be typed as a union in ruby: ```ruby field age T.nilable(T.any(::Integer,::String)) ``` # methodology-rust Source: https://speakeasy.com/docs/sdks/languages/rust/methodology-rust Coming soon! Stay tuned for updates. # TypeScript dependency management Source: https://speakeasy.com/docs/sdks/languages/typescript/dependency-management Generated TypeScript SDKs include dependencies that require ongoing maintenance to ensure security and stability. ## Set up automated dependency scanning We strongly recommend configuring a dependency scanning tool on your SDK repository. If your organization already uses a scanning tool, configure it for your SDK repository as well. Popular options include [Dependabot](https://docs.github.com/en/code-security/dependabot) (GitHub native), [Snyk](https://snyk.io/), and [Semgrep](https://semgrep.dev/). These tools automatically monitor your dependencies and create pull requests when updates are available. ## Keep dependencies updated For TypeScript SDKs, lock files like `package-lock.json` freeze dependency versions at SDK generation time. To refresh to the latest secure versions: ```bash rm -rf package-lock.json && rm -rf node_modules npm install ``` ## Adopt dependency cooldowns Consider implementing a dependency cooldown strategy where you wait a period (for example, 7-14 days) before adopting newly-published package versions. This practice helps protect against supply chain attacks. Recent incidents have shown that compromised packages are often caught and removed within the first few days of publication. A cooldown period allows the community to vet new releases before they enter your codebase. # TypeScript Feature Reference Source: https://speakeasy.com/docs/sdks/languages/typescript/feature-support import { Table } from "@/mdx/components"; ## Authentication
## Server Configuration
## Data Types ### Basic Types
### Polymorphism
## Methods
## Parameters
### Path Parameters Serialization
### Query Parameters Serialization
## Requests
## Responses
## Documentation
# Create TypeScript SDKs from OpenAPI / Swagger Source: https://speakeasy.com/docs/sdks/languages/typescript/methodology-ts import { Callout } from "@/mdx/components"; import { FileTree } from "nextra/components"; ## SDK Overview The Speakeasy TypeScript SDK creation builds idiomatic TypeScript libraries using standard web platform features. The SDK is strongly typed, makes minimal use of third-party modules, and is straightforward to debug. Using the SDKs that Speakeasy generates will feel familiar to TypeScript developers. We make opinionated choices in some places but do so thoughtfully and deliberately. The core features of the TypeScript SDK include: - Compatibility with vanilla JavaScript projects since the SDK's consumption is through `.d.ts` (TypeScript type definitions) and `.js` files. - Usable on the server and in the browser. - Use of the `fetch`, `ReadableStream`, and async iterable APIs for compatibility with popular JavaScript runtimes: - Node.js - Deno - Bun - Support for streaming requests and responses. - Authentication support for OAuth flows and support for standard security mechanisms (HTTP Basic, application tokens, and so on). - Optional pagination support for supported APIs. - Optional support for retries in every operation. - Complex number types including big integers and decimals. - Date and date/time types using RFC3339 date formats. - Custom type enums using strings and integers. - Union types and combined types. ## TypeScript Package Structure ## Runtime Environment Requirements The SDK targets ES2018, ensuring compatibility with a wide range of JavaScript runtimes that support this version. Key features required by the SDK include: - [Web Fetch API](https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API) - [Web Streams API](https://developer.mozilla.org/en-US/docs/Web/API/Streams_API) and in particular `ReadableStream` - [Async iterables](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Iteration_protocols#the_async_iterator_and_async_iterable_protocols) using `Symbol.asyncIterator` We used data from `caniuse` and `mdn` to determine our support policy and when we adopt a javascript feature. Typically we adopt features that have been out for > 3 years with reasonable support. Runtime environments that are explicitly supported are: - Evergreen browsers: Chrome, Safari, Edge, Firefox. - Node.js active and maintenance LTS releases (currently, v18 and v20). - Bun v1 and above. - Deno v1.39 - Note Deno doesn't currently have native support for streaming file uploads backed by the filesystem ([issue link][deno-file-streaming]). [deno-file-streaming](https://github.com/denoland/deno/issues/11018) For teams interested in working directly with the SDK's source files, our SDK leverages TypeScript `v5` features. To directly consume these source files, your environment should support TypeScript version 5 or higher. This requirement applies to scenarios where direct access to the source is necessary. ## TypeScript HTTP Client TypeScript SDKs stick as close to modern and ubiquitous web standards as possible. We use the `fetch()` API as our HTTP client. The API includes all the necessary building blocks to make HTTP requests: `fetch`, `Request`, `Response`, `Headers`, `FormData`, `File`, and `Blob`. The standard nature of this SDK ensures it works in modern JavaScript runtimes, including Node.js, Deno, Bun, and React Native. We've run our extensive suite to confirm that new SDKs work in Node.js, Bun, and browsers. ## Type System ### Primitive Types Where possible the TypeScript SDK uses primitive types such as `string`, `number`, and `boolean`. In the case of arbitrary-precision decimals, a third-party library is used since there isn't a native decimal type. Using decimal types is crucial in certain applications such as code manipulating monetary amounts and in situations where overflow, underflow, or truncation caused by precision loss can lead to significant incidents. To describe a decimal type in OpenAPI, use the `format: decimal` keyword. The SDK will take care of serializing and deserializing decimal values under the hood using the [decimal.js](https://github.com/MikeMcl/decimal.js) library. ```typescript import { SDK } from "@speakeasy/super-sdk"; import { Decimal } from "@speakeasy/super-sdk/types"; const sdk = new SDK(); const result = await sdk.payments.create({ amount: new Decimal(0.1).add(new Decimal(0.2)), }); ``` Similar to decimal types, we've introduced support for native `BigInt` values for numbers too large to be represented using the JavaScript `Number` type. In an OpenAPI schema, fields for big integers can be modeled as strings with `format: bigint`. ```typescript import { SDK } from "@speakeasy/super-sdk"; const sdk = new SDK(); const result = await sdk.doTheThing({ value: 67_818_454n, value: BigInt("340656901"), }); ``` ### Generated Types The TypeScript SDK generates a type for each request, response, and shared model in your OpenAPI schema. Each model is backed by a [Zod](https://zod.dev/) schema that validates the objects at runtime. It's important to note that data validation is run on user input when calling an SDK method _and on the subsequent response data from the server_. If servers are not returning data that matches the OpenAPI spec, then validation errors are thrown at runtime. Below is a complete example of a shared model created by the TypeScript generator: ## Model Structure Overview ### Public Type The `DrinkOrder` type represents the public type the model file exports and what SDK users will work with inside their code. ```typescript export type DrinkOrder = { id: string; type: DrinkType; customer: Customer; totalCost: Decimal$ | number; createdAt: Date; }; ``` ### Internal Types A special namespace accompanies every model and contains the types and schemas for the model that represent inbound and outbound data. > The namespace, including types and values in it, isn't intended for use outside the SDK and is marked as `@internal`. ```typescript /** @internal */ export namespace DrinkOrder$ { ``` ### Inbound The inbound representation of a model defines the shape of the data received from a server. It is validated and deserialized into the public type above. ```typescript export type Inbound = { id: string; type: DrinkType; customer: Customer$.Inbound; total_cost: string; created_at: string; }; export const inboundSchema: z.ZodType = z .object({ id: z.string(), type: DrinkType$, customer: Customer$.inboundSchema, total_cost: z.string().transform((v) => new Decimal$(v)), created_at: z .string() .datetime({ offset: true }) .transform((v) => new Date(v)), }) .transform((v) => { return { id: v.id, type: v.type, customer: v.customer, totalCost: v.total_cost, createdAt: v.created_at, }; }); ``` ### Outbound The outbound representation of a model defines the shape of the data sent to a server. A user provides a value that satisfies the public type above and the outbound schema serializes it into what the server expects. ```typescript export type Outbound = { id: string; type: DrinkType; customer: Customer$.Outbound; total_cost: string; created_at: string; }; export const outboundSchema: z.ZodType = z .object({ id: z.string(), type: DrinkType$, customer: Customer$.outboundSchema, totalCost: z .union([z.instanceof(Decimal$), z.number()]) .transform((v) => `${v}`), createdAt: z.date().transform((v) => v.toISOString()), }) .transform((v) => { return { id: v.id, type: v.type, customer: v.customer, total_cost: v.totalCost, created_at: v.createdAt, }; }); ``` ### Zod Validation All generated models have this overall structure. By pinning the types with runtime validation, Speakeasy gives users a stronger guarantee that the SDK types they work with during development are valid at runtime, otherwise, Speakeasy throws exceptions that fail loudly. ```typescript import { z } from "zod"; import { Decimal as Decimal$ } from "../../types"; import { Customer, Customer$ } from "./customer"; import { DrinkType, DrinkType$ } from "./drinktype"; export type DrinkOrder = { id: string; type: DrinkType; customer: Customer; totalCost: Decimal$ | number; createdAt: Date; }; /** @internal */ export namespace DrinkOrder$ { export type Inbound = { id: string; type: DrinkType; customer: Customer$.Inbound; total_cost: string; created_at: string; }; export const inboundSchema: z.ZodType = z .object({ id: z.string(), type: DrinkType$, customer: Customer$.inboundSchema, total_cost: z.string().transform((v) => new Decimal$(v)), created_at: z .string() .datetime({ offset: true }) .transform((v) => new Date(v)), }) .transform((v) => { return { id: v.id, type: v.type, customer: v.customer, totalCost: v.total_cost, createdAt: v.created_at, }; }); export type Outbound = { id: string; type: DrinkType; customer: Customer$.Outbound; total_cost: string; created_at: string; }; export const outboundSchema: z.ZodType = z .object({ id: z.string(), type: DrinkType$, customer: Customer$.outboundSchema, totalCost: z .union([z.instanceof(Decimal$), z.number()]) .transform((v) => `${v}`), createdAt: z.date().transform((v) => v.toISOString()), }) .transform((v) => { return { id: v.id, type: v.type, customer: v.customer, total_cost: v.totalCost, created_at: v.createdAt, }; }); } ``` Zod is bundled as a regular dependency in Speakeasy TypeScript SDKs. Speakeasy moved Zod from a peer dependency to a regular dependency to address Zod v4 runtime errors. This ensures the SDK always uses its own bundled Zod v3 even if the customer is using Zod v4, preventing version conflicts. Previously, there were situations where the `node_modules` tree could end up with multiple Zod versions if the user had a different version of Zod installed than what the SDK required. For example: ```text node_modules/ zod/ v3.21.1 sdk/ node_modules/ zod/ v3.23.5 ``` If the SDK threw a Zod validation error, the user might have code like this: ```typescript import { Sdk } from "sdk"; import { ZodError } from "zod"; const sdk = new Sdk(); try { const result = await sdk.drinks.list({ userId: "oops" }); } catch (err) { if (err instanceof ZodError) { // branch 1 console.error("the server returned invalid data"); } else { throw err; } } ``` The code at `branch 1` would not evaluate to `true` because the runtime loaded the wrong `ZodError` version. By bundling Zod as a regular dependency, Speakeasy prevents this validation error and ensures consistent behavior. Speakeasy SDKs wrap `ZodError`s with a custom error type declared in each SDK. In practice, the Zod error is hidden from users and gets them to only work with the error type exported from the SDK. ### Union Types Support for polymorphic types is critical to most production applications. In OpenAPI, these types are defined using the `oneOf` keyword. Speakeasy represents these types using TypeScript's union notation, for example, `Cat | Dog`. ```typescript import { SDK } from "@speakeasy/super-sdk"; async function run() { const sdk = new SDK(); const pet = await sdk.fetchMyPet(); switch (pet.type) { case "cat": console.log(pet.litterType); break; case "dog": console.log(pet.favoriteToy); break; default: // Ensures exhaustive switch statements in TypeScript pet satisfies never; throw new Error(`Unidentified pet type: ${pet.type}`); } } run(); ``` ### Type Safety TypeScript provides static type safety to give you greater confidence in the code you are shipping. However, TypeScript has limited support to protect from opaque data at the boundaries of your programs. User input and server data coming across the network can circumvent static typing if not correctly modeled. This usually means marking this data as `unknown` and exhaustively sanitizing it. Our TypeScript SDKs solve this issue neatly by modeling all the data at the boundaries using [Zod schemas](https://zod.dev/). Using Zod schemas ensures that everything coming from users and servers will work as intended, or fail loudly with clear validation errors. This is even more impactful for the vanilla JavaScript developers using your SDK. ```typescript import { SDK } from "@speakeasy/super-sdk"; async function run() { const sdk = new SDK(); const result = await sdk.products.create({ name: "Fancy pants", price: "ummm", }); } run(); // 🚨 Throws // // ZodError: [ // { // "code": "invalid_type", // "expected": "number", // "received": "string", // "path": [ // "price" // ], // "message": "Expected number, received string" // } // ] ``` While validating user input is considered table stakes for SDKs, it's especially useful to validate server data given the information we have in your OpenAPI spec. This can help detect drift between schema and server and prevent certain runtime issues such as missing response fields or sending incorrect data types. ## Tree Shaking Speakeasy-created Typescript SDKs contain few internal couplings between modules. Users who bundle them into client-side apps can take advantage of tree-shaking performance when working with "deep" SDKs. These SDKs are subdivided into namespaces like `sdk.comments.create(...)` and `sdk.posts.get(...)`. Importing the top-level SDK pulls the entire SDK into a client-side bundle even if a small subset of functionality was needed. You can import the exact namespaces or "sub-SDKs", and tree-shake the rest of the SDK away at build time. ```typescript import { PaymentsSDK } from "@speakeasy/super-sdk/sdk/payments"; // 👆 Only code needed by this SDK is pulled in by bundlers async function run() { const payments = new PaymentsSDK({ authKey: "" }); const result = await payments.list(); console.log(result); } run(); ``` Speakeasy benchmarked whether there would be benefits in allowing users to import individual SDK operations but from our testing, there was a marginal reduction in bundled code versus importing sub-SDKs. It's highly dependent on how operations are grouped and the depth and breadth of an SDK as defined in the OpenAPI spec. If you think your SDK users could greatly benefit from exporting individual operations, please reach out to us and we can re-evaluate this feature. ## Streaming Support Support for streaming is critical for applications that need to send or receive large amounts of data between client and server without first buffering the data into memory, potentially exhausting this system resource. Uploading a huge file is one use case where streaming can be useful. As an example, in Node.js v20, streaming a large file to a server using an SDK is only a handful of lines: ```typescript import { openAsBlob } from "node:fs"; import { SDK } from "@speakeasy/super-sdk"; async function run() { const sdk = new SDK(); const fileHandle = await openAsBlob("./src/sample.txt"); const result = await sdk.upload({ file: fileHandle }); console.log(result); } run(); ``` In the browser, users would typically select files using `` and the SDK call is identical to the sample code above. Other JavaScript runtimes may have similar native APIs to obtain a web standards file or blob and pass it to SDKs. For response streaming, SDKs expose a `ReadableStream`, a part of the Streams API web standard. ```typescript import fs from "node:fs"; import { Writable } from "node:stream"; import { SDK } from "@speakeasy/super-sdk"; async function run() { const sdk = new SDK(); const result = await sdk.usageReports.download("UR123"); const destination = Writable.toWeb(fs.createWriteStream("./report.csv")); await result.data.pipeTo(destination); } run(); ``` ## Server-Sent Events TypeScript SDKs support the streaming of server-sent events by exposing async iterables. Unlike the native `EventSource` API, SDKs can create streams using GET or POST requests, and other methods that can pass custom headers and request bodies. ```typescript import { SDK } from "@speakeasy/super-sdk"; async function run() { const sdk = new SDK(); const result = await sdk.completions.chat({ messages: [ { role: "user", content: "What is the fastest bird that is common in North America?", }, ], }); if (result.chatStream == null) { throw new Error("failed to create stream: received null value"); } for await (const event of result.chatStream) { process.stdout.write(event.data.content); } } run(); ``` For more information on how to model this API in your OpenAPI document, see [Enabling Event-Streaming Operations](/docs/customize-sdks/server-sent-events). ## Parameters If configured, Speakeasy generates methods with parameters for each parameter defined in the OpenAPI document, provided the number of parameters is less than or equal to the configured `maxMethodParams` value in the `gen.yaml` file. If the number of parameters exceeds the configured `maxMethodParams` value or is set to `0`, then a request object is generated for the method instead allowing all parameters to be passed in a single object. ## Errors Following TypeScript best practices, all operation methods in the SDK will return a response object and an error. Callers should always check for the presence of the error. The object used for errors is configurable per request. Any error response may return a custom error object. A generic error will be provided when any sort of communication failure is detected during an operation. Here's an example of custom error handling in a theoretical SDK: ```typescript import { Speakeasy } from "@speakeasy/bar"; import * as errors from "@speakeasy/bar/sdk/models/errors"; async function run() { const sdk = new Speakeasy({ apiKey: "", }); const res = await sdk.bar.getDrink().catch((err) => { if (err instanceof errors.FailResponse) { console.error(err); // handle exception return null; } else { throw err; } }); if (res?.statusCode !== 200) { throw new Error("Unexpected status code: " + res?.statusCode || "-"); } // handle response } run(); ``` The SDK also includes a `SDKValidationError` to make it easier to debug validation errors, particularly when the server sends unexpected data. Instead of throwing a `ZodError` back at SDK users without revealing the underlying raw data that failed validation, `SDKValidationError` provides a way to pretty-print validation errors for a more pleasant debugging experience. ## Debugging Support Typescript SDKs support a new response format that includes the native Request and Response objects that were used in an SDK method call. Enable this by setting the `responseFormat` config in your `gen.yaml` file to `envelope-http`. ```typescript const sdk = new SDK(); const { users, httpMeta } = await sdk.users.list(); // 👆 const { request, response } = httpMeta; console.group("Request completed"); console.log("Endpoint:", request.method, request.url); console.log("Status", response.status); console.log("Content type", response.headers.get("content-type")); console.groupEnd(); ``` The `httpMeta` property will also be available on any error class that relates to HTTP requests. This includes the built-in `SDKError` class and any custom error classes that you have defined in your spec. ## User Agent Strings The Typescript SDK includes a [`User-Agent`](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/User-Agent) string in all requests to track SDK usage amongst broader API usage. The format is as follows: ```stmpl speakeasy-sdk/typescript {{SDKVersion}} {{GenVersion}} {{DocVersion}} {{PackageName}} ``` Where - `SDKVersion` is the version of the SDK, defined in `gen.yaml` and released. - `GenVersion` is the version of the Speakeasy generator. - `DocVersion` is the version of the OpenAPI document. - `PackageName` is the name of the package defined in `gen.yaml`. # Install the CLI if you haven't Source: https://speakeasy.com/docs/sdks/languages/typescript/migrating-from-oss import { Callout } from "@/mdx/components"; If you use [OpenAPI Generator to create TypeScript Node SDKs](https://openapi-generator.tech/docs/generators/typescript-node), you're probably familiar with its challenges: although it's a neat tool to generate basic SDKs from OpenAPI, its features, documentation, error handling, and general support are limited. This guide walks you through migrating from OpenAPI Generator's TypeScript Node generator to Speakeasy and provides practical tips to make the transition smooth for you and your API consumers. ## Why migrate to Speakeasy? In [our comparison of OpenAPI Generator and Speakeasy](/docs/languages/typescript/oss-comparison-ts), we highlight the benefits of using Speakeasy to generate SDKs. Here's a quick recap: - **Advanced schema validation:** Compared to the basic validation OSS generators provide, Speakeasy uses Zod for robust, type-safe schema validation. - **Comprehensive documentation generation:** Speakeasy autogenerates full documentation with examples, keeping docs in sync with the SDK. - **Enhanced union types and polymorphism:** Speakeasy supports discriminated unions and offers improved type safety for polymorphic schemas. - **Built-in support for OAuth 2.0 and retries:** Out-of-the-box features like OAuth 2.0 flows, automatic retries, and pagination simplify error handling and improve the developer experience. - **Modern ecosystem compatibility:** Speakeasy is designed for modern development environments (including support for React Hooks, Deno, Bun, and more) and integrates easily with CI/CD pipelines. ## Migration example: A TypeScript SDK We'll use a coffee API as a running example to demonstrate migrating a TypeScript SDK from OpenAPI Generator to Speakeasy. We used the OpenAPI document to create a TypeScript Node SDK that allows users to interact with the coffee API. Here's a simplified version of the OpenAPI document: ```yaml openapi: 3.1.0 info: title: Coffee Orders API version: 1.0.0 description: A CRUD API for managing coffee orders and available coffee types. security: - ApiKeyAuth: [] servers: - url: http://localhost:8000 description: Development server paths: /orders: get: tags: - Orders summary: Get Orders operationId: GetOrders parameters: - name: coffee_type in: query required: false schema: type: string responses: 200: description: Successful Response content: application/json: schema: type: array items: $ref: "#/components/schemas/CoffeeOrder" x-speakeasy-group: orders /coffee-types: get: tags: - CoffeeTypes summary: Get Coffee Types operationId: GetCoffeeTypes responses: 200: description: Successful Response content: application/json: schema: type: array items: $ref: "#/components/schemas/CoffeeType" x-speakeasy-group: coffeeTypes components: schemas: CoffeeType: properties: name: type: string description: type: string id: type: integer price_multiplier: type: number type: object required: - id - name ``` For the complete example project, including the generated open-source SDK and OpenAPI document, check out the [coffee API example repository](https://github.com/speakeasy-api/speakeasy-examples/oss-migration-guide). ## What about all my custom code? Odds are, you've customized your OpenAPI Generator TypeScript SDK extensively, maintaining a mix of generated code and custom layers for specialized error handling, logging middleware, and helper functions. This leads to a question: "How difficult will it be to implement all of my custom functionality?" With OpenAPI Generator, these customizations often live outside the generated code or require you to modify the generated files directly, making regenerating the SDK tedious and error-prone. The good news is that Speakeasy offers built-in solutions for most custom enhancements through its hooks system, which we explore below. Instead of hacking generated code (which breaks every time you regenerate), you can extend the SDK predictably and maintainably. ## Migrating your TypeScript SDK from OpenAPI Generator to Speakeasy Let's take a step-by-step look at migrating the SDK to Speakeasy, highlighting the key differences and improvements along the way. ### 1. Pre-migration checklist You'll need: - **A Speakeasy account.** Sign up free at [Speakeasy.com](https://speakeasy.com/). - **The Speakeasy CLI.** Install the Speakeasy CLI globally in your environment. ```bash curl -fsSL https://go.speakeasy.com/cli-install.sh | sh ``` Authenticate the CLI with your Speakeasy account: ```bash speakeasy auth login ``` - **An OpenAPI document.** Make sure you have the latest version of your OpenAPI document. If not, export it from your API documentation or source code. We use the coffee API OpenAPI document to demonstrate. - **SDK feature inventory.** List custom features in your SDK that aren't part of the standard OpenAPI Generator output, for example, custom authentication mechanisms, rate limiting logic, or extended error classes. | Feature | Implementation | Used by customers? | | -------------- | ----------------------- | ------------------ | | Authentication | Basic API key in header | Yes, widely | | Rate limiting | Custom retry logic | Yes, some | | Error handling | Extended error classes | Yes, power users | Don't skip this step! Understanding what custom features your SDK has helps prioritize what needs special attention during migration. ### 2. Generate a Speakeasy SDK using your OpenAPI document The Speakeasy CLI provides a quickstart command that guides you through the process of importing your OpenAPI document and generating a Speakeasy SDK. Alternatively, you can create a workflow file manually and run the generation command. We'll use the quickstart command. If you're following along with the coffee API example, you'll need to `git clone` the [example repository](https://github.com/speakeasy-api/speakeasy-examples) and navigate to the `oss-migration-guide` directory to run the quickstart command. ```bash brew install speakeasy-api/tap/speakeasy # Run the quickstart command speakeasy quickstart ``` The quickstart process walks you through importing your OpenAPI document and configuring your SDK, including selecting the language, naming the SDK, and specifying the directory the SDK should be output to. Select TypeScript as the output language. Speakeasy generates your TypeScript SDK from the OpenAPI document and writes it to the specified directory. ![Speakeasy SDK generation](/assets/docs/speakeasy-quickstart.png) ### 3. Compare the generated SDKs Let's compare key aspects of the SDKs generated by OpenAPI Generator and Speakeasy to understand how they affect your users. #### Authentication Speakeasy offers a more straightforward security configuration that's directly tied to the SDK instance itself, making it easier for your users to manage authentication:
```typescript filename="OpenAPI Generator" import { Configuration, DefaultApi } from 'coffee-api-sdk'; const config = new Configuration({ apiKey: 'YOUR_API_KEY' }); const api = new DefaultApi(config); ```
```typescript filename="Speakeasy" import { SpeakeasyCoffeeClient } from "speakeasy-coffee-client"; const sdk = new SpeakeasyCoffeeClient({ apiKeyAuth: "YOUR_API_KEY" }); ```
#### API calls Speakeasy's method naming convention reflects the API's path structure more explicitly. While this approach is somewhat more verbose, it's helpful for understanding API relationships:
```typescript filename="OpenAPI Generator" // Flat structure const orders = await api.getOrders({ coffeeType: 'Latte' }); const order = await api.getOrder(123); ```
```typescript filename="Speakeasy" // Resource-based organization const orders = await sdk.orders.list({ coffeeType: 'Latte' }); const order = await sdk.orders.getById({ orderId: 123 }); ```
#### Error handling Speakeasy provides typed errors that allow for granular error handling, meaning your users can catch specific error types and respond accordingly:
```typescript filename="OpenAPI Generator" // Generic errors try { await api.createOrder(newOrder); } catch (error) { console.error('API error:', error.message); } ```
```typescript filename="Speakeasy" // Typed errors try { await sdk.orders.create({ id: 5, customerName: "Alice", coffeeType: "Espresso", size: "Medium", extras: ["Extra shot"], price: 4.50 }); } catch (error) { if (error instanceof sdk.Error.BadRequestError) { console.error('Invalid request:', error.statusCode, error.body); } else if (error instanceof sdk.Error.RateLimitError) { console.error('Rate limit exceeded, retry after:', error.headers.get('retry-after')); } else { console.error('Unknown error:', error); } } ```
A key benefit of Speakeasy is that most features are built-in, reducing the need for custom code. With OpenAPI Generator, you often need to write additional code to handle common scenarios like authentication, error handling, and pagination. ### 4. Test the generated SDK By default, OpenAPI Generator doesn't provide tests for your SDK. You'll need to write your own tests to validate the generated code. For example: ```typescript filename="./custom-test.ts" import { Configuration, DefaultApi } from "coffee-api-sdk"; import { expect, test } from "vitest"; test("get orders returns correct data", async () => { const config = new Configuration({ apiKey: process.env.API_KEY, }); const api = new DefaultApi(config); const orders = await api.getOrders(); expect(orders).toBeDefined(); expect(orders.length).toBeGreaterThan(0); }); ``` By contrast, Speakeasy can generate a baseline suite of tests that validate core functionality (when test generation is enabled), reducing the initial testing overhead. To add tests to your Speakeasy SDK, run the following command from the SDK directory: ```bash speakeasy configure tests ``` ![Configuring tests](/assets/docs/speakeasy-configure-tests.png) Speakeasy generates a `__tests__` directory in the SDK's `/src` folder, containing baseline tests to validate core functionality. ```typescript filename="./src/__tests__/orders.test.ts" test("Orders Get Orders Multiple Orders", async () => { const testHttpClient = createTestHTTPClient("GetOrders-multiple_orders"); const speakeasyCoffeeClient = new SpeakeasyCoffeeClient({ serverURL: process.env["TEST_SERVER_URL"] ?? "http://localhost:18080", httpClient: testHttpClient, apiKeyAuth: "your-api-key-here", }); const result = await speakeasyCoffeeClient.orders.list({}); expect(result).toBeDefined(); expect(result).toEqual([ { id: 1, customerName: "Alice", coffeeType: "Latte", size: "Medium", extras: ["Extra shot", "Soy milk"], price: 4.5, }, { id: 2, customerName: "Bob", coffeeType: "Espresso", size: "Small", extras: ["Extra shot"], price: 3.5, }, ]); }); ``` You'll need to write additional tests to cover any custom functionality in your SDK. ```typescript filename="custom-test.ts" // Custom test test("SpeakeasyCoffeeClient - Rate Limiting", async () => { const testHttpClient = createTestHTTPClient("rate_limiting"); const sdk = new SpeakeasyCoffeeClient({ serverURL: process.env["TEST_SERVER_URL"] ?? "http://localhost:8000", httpClient: testHttpClient, }); const result = await sdk.orders.list({}); expect(result).toBeDefined(); expect(result.orders?.length).toBeGreaterThan(0); expect(result.orders?.[0]).toEqual({ id: 1, customerName: "Alice", coffeeType: "Latte", size: "Medium", extras: ["Extra shot", "Soy milk"], price: 4.5, }); }); ``` ### 5. Implement your migration strategy How to migrate your customers to the new SDK can be a tricky decision and depends on your customer base, the complexity of your SDK, and your team's capacity. Most teams choose one of two approaches: Side-by-side implementation or versioned migration. #### Option A: Side-by-side implementation A side-by-side implementation works well for SDKs with many users who need time to migrate, allowing you to maintain both the OpenAPI Generator and Speakeasy versions of your SDK in parallel. Here's how a side-by-side implementation works: 1. Generate both SDKs in your build pipeline. 2. Package the Speakeasy SDK as a "beta" or "next" version. 3. Encourage users to adopt the new SDK. 4. **Eventually** deprecate the OpenAPI Generator SDK. ```text filename="Example package structure" coffee-api-sdk (original) coffee-api-sdk-next (Speakeasy version) ``` #### Option B: Versioned migration (recommended) For a cleaner approach, release the Speakeasy version of your SDK as a **new major version**. ```json filename="package.json" { "name": "coffee-api-sdk", "version": "2.0.0", "description": "Now powered by Speakeasy" } ``` Here's how a versioned migration works: 1. Clearly document breaking changes. 2. Provide migration examples. 3. Support customers through the transition. This approach is typically recommended for most teams, as it avoids the maintenance burden of two parallel implementations. ## Preserving custom functionality in your SDK One of the biggest challenges in migrating an SDK from OpenAPI Generator is preserving custom code. Speakeasy simplifies this with SDK hooks, which allow you to inject custom logic at specific points in the request lifecycle: - **On SDK initialization:** Configure base URLs, auth settings, or custom HTTP clients. - **Before request:** Add headers, validate data, or implement logging. - **After success:** Process successful responses or add telemetry. - **After error:** Customize error handling or add debugging information. ### Comparing customization approaches: OpenAPI Generator vs Speakeasy With OpenAPI Generator, you typically modify generated code directly or create wrapper classes – both of which can be lost when you regenerate the SDK. Speakeasy's approach is much more maintainable. For example, if we want to add analytics to our SDK, we can create a custom hook that logs the time taken for each API call:
```typescript filename="OpenAPI Generator" class CoffeeApiWithAnalytics extends DefaultApi { constructor(config: Configuration) { super(config); } async getOrders(options?: any): Promise { const startTime = Date.now(); try { const result = await super.getOrders(options); console.log(`API call completed in ${Date.now() - startTime}ms`); return result; } catch (error) { console.error(`API call failed`); throw error; } } } ```
```typescript filename="Speakeasy" import { AfterSuccessHook, BeforeRequestHook } from "./types"; export class AnalyticsHook implements BeforeRequestHook, AfterSuccessHook { beforeRequest(ctx, request) { ctx.context.set("startTime", Date.now()); ctx.context.set("path", new URL(request.url).pathname); return request; } afterSuccess(ctx, response) { const startTime = ctx.context.get("startTime"); const path = ctx.context.get("path"); console.log(`${path} completed in ${Date.now() - startTime}ms`); return response; } } ```
With Speakeasy, you can create a custom hook that implements the `BeforeRequestHook` and `AfterSuccessHook` interfaces, allowing you to add custom logic without modifying the generated code directly. To use your custom hook, you need to register it in your SDK configuration: ```typescript filename="src/hooks/registration.ts" import { AnalyticsHook } from "./analytics"; import { Hooks } from "./types"; export function initHooks(hooks: Hooks) { const analyticsHook = new AnalyticsHook(); hooks.registerBeforeRequestHook(analyticsHook); hooks.registerAfterSuccessHook(analyticsHook); } ``` Speakeasy's SDK hooks make customizations: - **Maintainable:** Hooks are kept separate from generated code. - **Reusable:** One hook implementation works across all API methods. - **Future-proof:** Your customizations survive SDK regeneration. - **Testable:** Hooks can be independently unit tested. If your hooks need external packages (like a logging library), you can add them to your `gen.yaml` configuration: ```yaml typescript: additionalDependencies: dependencies: winston: "^3.17.0" # For logging ``` For more details on implementing various hook types, refer to the [SDK hooks documentation](/docs/customize/code/sdk-hooks). ## Automating SDK generation, testing, and publishing with CI/CD OpenAPI Generator lacks built-in automation for SDK generation, testing, and publishing. Speakeasy integrates seamlessly with CI/CD pipelines, allowing you to automate the entire process from generating the SDK to publishing it to npm. Explore the [Speakeasy Platform](https://app.speakeasy.com/) to learn how it helps you automate SDK generation and deployment. ## Supporting your users through migration Migrating your SDK is only half the battle – you also need to support your users through the transition. The best way to do this is by providing clear documentation that explains the migration process. For example: ```markdown filename="MIGRATION.md" # Migrating to SDK v2.0 ## Key Changes ### Authentication **Before:** \```typescript const config = new Configuration({ apiKey: 'YOUR_API_KEY' }); const api = new DefaultApi(config); \``` **After:** \```typescript const sdk = new SpeakeasyCoffeeClient({ apiKeyAuth: 'YOUR_API_KEY' }); \``` ### Making API Calls **Before:** \```typescript const orders = await api.getOrders({ coffeeType: 'Latte' }); const order = await api.getOrder(123); \``` **After:** \```typescript const orders = await sdk.orders.list({ coffeeType: 'Latte' }); const order = await sdk.orders.getById({ orderId: 123 }); \``` ### Error Handling **Before:** Generic catch-all errors **After:** Typed errors (`sdk.Error.BadRequestError`, etc.) ``` Speakeasy automatically generates SDK documentation, reducing the effort required on your end. In addition to generated docs, provide migration guides and examples to help customers understand the changes. ## Summary Here's a quick recap of the steps involved in migrating an SDK from OpenAPI Generator to Speakeasy: - Start with a thorough inventory of your current setup. - Test the migrated SDK thoroughly, especially any custom functionality. - Decide on a migration strategy that fits your customer base. - Provide clear documentation of all changes to your customers. # Comparing OpenAPI TypeScript SDK Generators Source: https://speakeasy.com/docs/sdks/languages/typescript/oss-comparison-ts import { Table } from "@/mdx/components"; At Speakeasy, [idiomatic SDKs](/docs/languages/philosophy) are created in a variety of languages, with generators that follow principles ensuring SDKs the best developer experience. The goal is to let developers focus on building great APIs and applications, without being distracted by hand-rolling custom SDKs just to get basic functionality. In this post, we'll compare TypeScript SDKs managed by Speakeasy to those generated by open-source generators. ## The TypeScript SDK generator landscape We'll compare the Speakeasy SDK generator to some popular popular open-source generators. Our evaluation includes: 1. The [TypeScript Fetch](https://openapi-generator.tech/docs/generators/typescript-fetch/) generator from OpenAPI Generators. 2. The [TypeScript Node](https://openapi-generator.tech/docs/generators/typescript-node/) generator from OpenAPI Generators. 3. [Oazapfts](https://github.com/oazapfts/oazapfts), an open-source generator with over 500 stars on GitHub. 4. The [Speakeasy SDK generator](/docs/speakeasy-reference/cli/getting-started). Here's the summary of how the different generators compare:
For a detailed comparison, read on. ## Installing SDK generators Although generator installation does not impact the resulting SDKs, your team will install the generator on each new development environment. We believe an emphasis on usability starts at home, and your internal tools should reflect this. Install the Speakeasy CLI by running the Homebrew install command for macOS, or see the [installation instructions](/docs/speakeasy-reference/cli/getting-started) for other platforms: ```bash brew install speakeasy-api/tap/speakeasy ``` Installing `openapi-generator` using Homebrew installs `openjdk@11` and its numerous dependencies: ```bash brew install openapi-generator ``` Installing oazapfts is easiest done as an Node.js module with NPM or similar: ```bash # Install oazapfts as a dependency npm install oazapfts --save ``` These generators will need an OpenAPI document to work with. A common OpenAPI document used for testing all sorts of OpenAPI tooling is the [Train Travel API](https://github.com/bump-sh-examples/train-travel-api). Start by downloading the YAML from [https://raw.githubusercontent.com/bump-sh-examples/train-travel-api/refs/heads/main/openapi.yaml](https://raw.githubusercontent.com/bump-sh-examples/train-travel-api/refs/heads/main/openapi.yaml) to the working directory. ```bash wget https://raw.githubusercontent.com/bump-sh-examples/train-travel-api/refs/heads/main/openapi.yaml ``` ## Document validation Both the OpenAPI Generator and Speakeasy CLI can validate an OpenAPI document to make sure it's valid and well-formed. Oazapfts doesn't offer document validation, so a separate validation step is needed to use it at scale. To validate `openapi.yaml` using OpenAPI Generator, run the following in the terminal: ```bash openapi-generator validate -i openapi.yaml ``` The OpenAPI Generator validator returns the following output: ``` Validating spec (openapi.yaml) No validation issues detected. ``` ### Validation using Speakeasy We'll validate the spec with Speakeasy by running the following in the terminal: ```bash speakeasy validate openapi -s openapi.yaml ``` The Speakeasy validator returns one warning, and some hints reminding the author to add examples. Each warning or hint includes a detailed, structured error with line numbers to help us fix anything that needs fixing. Since the Speakeasy validator produced only a warning and hints, we can assume that all our generators will generate SDKs without issues. Here's how the generators' validation features compare:
## Generating SDKs Now that the OpenAPI document has been confirmed valid, it's time to start generating and comparing SDKs. First, create an SDK using Speakeasy, and take a brief look at its structure. Then generate SDKs using the other generators, and compare the generated code to the Speakeasy SDK. ### Generating an SDK using Speakeasy To create a TypeScript SDK using the Speakeasy CLI, run the following in the terminal: ```bash speakeasy quickstart ``` It will ask a few questions about the SDK we want to create, including the OpenAPI document (`openapi.yaml`), the name of the SDK (`TrainTravel`), the language/framework which will be TypeScript, and a package name for publishing to NPM (`train-travel-sdk`). Then pick an output directory for the SDK, for example `train-travel-sdk`. That's it! The Speakeasy CLI generates the SDK, turns it into a Git repository if requested, and creates the following file structure: ``` ├── CONTRIBUTING.md ├── FUNCTIONS.md ├── README.md ├── RUNTIMES.md ├── USAGE.md ├── dist │ ├── commonjs │ ├── esm │ └── node_modules ├── docs │ ├── lib │ ├── models │ └── sdks ├── eslint.config.mjs ├── examples │ ├── node_modules │ ├── package-lock.json │ ├── package.json │ ├── README.md │ └── stationsGetStations.example.ts ├── src │ ├── core.ts │ ├── funcs │ ├── hooks │ ├── index.ts │ ├── lib │ ├── models │ ├── sdk │ └── types └── tsconfig.json ``` At a glance, we can see that Speakeasy creates documentation for each model in the API description. It also creates a full-featured NPM package, with all the Markdown files you'd expect to see in any open-source project. Code is split between internal tools and the SDK code, and comes packaged ready for distribution to NPM with support for CommonJS and ES Modules. We'll start poking around the code to get a feel for how it all works, but first, let's generate SDKs using the other generators. ### Generating SDKs using OpenAPI Generator OpenAPI Generator is an open-source collection of community-maintained generators. It features generators for a wide variety of client languages, and for some languages, there are multiple generators. TypeScript tops this list of languages with multiple generators, with 11 options to choose from. The two TypeScript SDK generators from OpenAPI Generator covered here are [typescript-fetch](https://openapi-generator.tech/docs/generators/typescript-fetch/) and [typescript-node](https://openapi-generator.tech/docs/generators/typescript-node/). Both generators are very similar, but the `typescript-fetch` generator creates SDKs that work in both browser and Node.js environments, while the `typescript-node` generator creates SDKs optimized for Node.js environments. There is no interactive CLI for OpenAPI Generator, so there are no prompts to guide you on the way. Instead you'll do the whole thing with command line arguments: ```bash # Generate Train Travel SDK using typescript-fetch generator openapi-generator generate \ --input-spec openapi.yaml \ --generator-name typescript-fetch \ --output ./train-travel-sdk-typescript-fetch \ --additional-properties=npmName=train-travel-sdk-typescript-fetch # Generate Train Travel SDK using typescript-node generator openapi-generator generate \ --input-spec openapi.yaml \ --generator-name typescript-node \ --output ./train-travel-sdk-typescript-node \ --additional-properties=npmName=train-travel-sdk-typescript-node ``` Once run there will be lots of output as OpenAPI Generator churns through the document and generates the SDK, with warnings and output about unsafe access to caffeine... but that's just Java being Java. Ignore all that and look for something like: ``` # Thanks for using OpenAPI Generator. # We appreciate your support! ``` If they both worked there will be a list of files generated in each output directory. Let's take a look at the file structure of each generated SDK. The `typescript-fetch` generator creates the following file structure. There is no documentation or examples included, nor contributing guides or other supporting internal Markdown files. Only a README and the code itself are included. ``` # train-travel-sdk-typescript-fetch ├── package.json ├── README.md ├── src │ ├── apis │ ├── index.ts │ ├── models │ └── runtime.ts └── tsconfig.json ``` The `typescript-node` generator a much flatter structure, with no `src/` directory, just an `api` and `model` directory. Similar to the `typescript-fetch` generator, there is no documentation or examples of any sort, and not even a README. ``` # train-travel-sdk-typescript-node ├── api ├── api.ts ├── model ├── package.json └── tsconfig.json ``` The code structure is quite different between the two generators, but looking through that comes a little later. There's one more generator to try out. ### Generating an SDK with oazapfts Oazapfts is essentially a thin wrapper around [Fetch API](https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API/Using_Fetch) with TypeScript type definitions. The SDK is generated as a single file, with no documentation, no examples, no package structure at all, so it's all super minimalistic. When oazapfts has been added to a project with `npm install`, it can be called with `npm exec`. This will take the OpenAPI document as one argument and the output `.ts` file as a second argument. ```bash npm exec oazapfts openapi.yaml index.ts ``` The output TypeScript file `index.ts` will rely on `oazapfts` as a runtime dependency, which provides the necessary functionality for the SDK. ```ts import * as Oazapfts from "oazapfts/lib/runtime"; import * as QS from "oazapfts/lib/runtime/query"; ``` Code generated by oazapfts excludes the HTTP client code, error handling, and serialization. This means that oazapfts relies on the runtime library to provide these features. This keeps the generated code small, but it also means that the SDK cannot be used without the runtime library and its dependencies. ## Comparing generated code Let's take a look at how each of the SDK generators handles the same OpenAPI document, and seeing as this is TypeScript lets start with type definitions. To keep things interesting the example we'll focus on is a polymorphic model. Polymorphism is about representing different types that share a common interface. In OpenAPI, [polymorphic objects](/openapi/schemas/objects/polymorphism) are represented using `oneOf` sub-schemas and sometimes the `discriminator` object. Here's a slightly trimmed down example from the Train Travel API: ```yaml # components.schemas. BookingPayment: type: object properties: amount: type: number description: Amount intended to be collected by this payment. A positive decimal figure describing the amount to be collected. currency: $ref: '#/components/schemas/Currency' description: Three-letter [ISO currency code](https://www.iso.org/iso-4217-currency-codes.html), in lowercase. source: oneOf: - title: Card type: object properties: object: type: string const: card name: type: string number: type: string cvc: type: string writeOnly: true exp_month: type: integer format: int64 exp_year: type: integer format: int64 address_post_code: type: string required: - name - number - cvc - exp_month - exp_year - title: Bank Account type: object properties: object: const: bank_account type: string name: type: string number: type: string sort_code: type: string bank_name: type: string required: - name - number - bank_name ``` How will each generator handle this polymorphic object? Let's find out! ### Speakeasy type definitions Speakeasy will generate union types for polymorphic objects, and use the discriminator to add runtime type casting for input and output objects. The following code is a snippet of types generated for the `BookingPayment` schema: ```ts /** * Three-letter [ISO currency code](https://www.iso.org/iso-4217-currency-codes.html), in lowercase. */ export const Currency = { Bam: "bam", Bgn: "bgn", Chf: "chf", Eur: "eur", Gbp: "gbp", Nok: "nok", Sek: "sek", Try: "try", } as const; /** * Three-letter [ISO currency code](https://www.iso.org/iso-4217-currency-codes.html), in lowercase. */ export type Currency = ClosedEnum; /** * A bank account to take payment from. Must be able to make payments in the currency specified in the payment. */ export type BankAccount = { object?: "bank_account" | undefined; name: string; /** * The account number for the bank account, in string form. Must be a current account. */ number: string; /** * The sort code for the bank account, in string form. Must be a six-digit number. */ sortCode?: string | undefined; /** * The name of the bank associated with the routing number. */ bankName: string; }; /** * A card (debit or credit) to take payment from. */ export type Card = { object?: "card" | undefined; /** * Cardholder's full name as it appears on the card. */ name: string; /** * The card number, as a string without any separators. On read all but the last four digits will be masked for security. */ number: string; /** * Card security code, 3 or 4 digits usually found on the back of the card. */ cvc: string; /** * Two-digit number representing the card's expiration month. */ expMonth: number; /** * Four-digit number representing the card's expiration year. */ expYear: number; /** * Postal code associated with the card's billing address. */ addressPostCode?: string | undefined; }; /** * The payment source to take the payment from. This can be a card or a bank account. Some of these properties will be hidden on read to protect PII leaking. */ export type Source = Card | BankAccount; /** * A payment for a booking. */ export type BookingPayment = { /** * Amount intended to be collected by this payment. A positive decimal figure describing the amount to be collected. */ amount?: number | undefined; /** * Three-letter [ISO currency code](https://www.iso.org/iso-4217-currency-codes.html), in lowercase. */ currency?: Currency | undefined; /** * The payment source to take the payment from. This can be a card or a bank account. Some of these properties will be hidden on read to protect PII leaking. */ source?: Card | BankAccount | undefined; }; ``` Reusing the descriptions as comments means the code is nicely decorated for anyone who goes prodding around, and by using docblock syntax it will be read by JS/TS documentation generators too. The types are also defined and exported so they can be used in runtime code easily, instead of defined inline as many of the other generators do. This helps reuse throughout the rest of the SDK for request/responses, and allow for the most complex of scenarios to be handles easily. ```ts export type CreateBookingPaymentResponseBody$Outbound = { id?: string | undefined; amount?: number | undefined; currency?: string | undefined; source?: Card$Outbound | BankAccount$Outbound | undefined; status?: string | undefined; links?: models.LinksBooking$Outbound | undefined; }; ``` ### Oazapfts type definition Over to oazapfts, which sticks to its minimalist approach and generates one type for the request and one type for the response, with anything inside that being defined in line. If there are lots of shared parameters between requests and responses then these will be repeated, and that makes documentation and code suffer, but it keeps things simple. As for polymorphism, oazapfts handles union types with runtime type casting. ```ts export type BookingPaymentRead = { /** Unique identifier for the payment. This will be a unique identifier for the payment, and is used to reference the payment in other objects. */ id?: string; /** Amount intended to be collected by this payment. A positive decimal figure describing the amount to be collected. */ amount?: number; /** Three-letter [ISO currency code](https://www.iso.org/iso-4217-currency-codes.html), in lowercase. */ currency?: "bam" | "bgn" | "chf" | "eur" | "gbp" | "nok" | "sek" | "try"; /** The status of the payment, one of `pending`, `succeeded`, or `failed`. */ status?: "pending" | "succeeded" | "failed"; }; export type BookingPaymentWrite = { /** Amount intended to be collected by this payment. A positive decimal figure describing the amount to be collected. */ amount?: number; /** Three-letter [ISO currency code](https://www.iso.org/iso-4217-currency-codes.html), in lowercase. */ currency?: "bam" | "bgn" | "chf" | "eur" | "gbp" | "nok" | "sek" | "try"; /** The payment source to take the payment from. This can be a card or a bank account. Some of these properties will be hidden on read to protect PII leaking. */ source?: { "object"?: "card"; /** Cardholder's full name as it appears on the card. */ name: string; /** The card number, as a string without any separators. On read all but the last four digits will be masked for security. */ "number": string; /** Card security code, 3 or 4 digits usually found on the back of the card. */ cvc: string; /** Two-digit number representing the card's expiration month. */ exp_month: number; /** Four-digit number representing the card's expiration year. */ exp_year: number; /** The postal code associated with the card's billing address. */ address_post_code?: string; } | { "object"?: "bank_account"; name: string; /** The account number for the bank account, in string form. Must be a current account. */ "number": string; /** The sort code for the bank account, in string form. Must be a six-digit number. */ sort_code?: string; /** The name of the bank associated with the routing number. */ bank_name: string; }; }; ``` The verbosity of these types can be improved with the `--mergeReadWriteOnly` to combine the read and write models into one, but similar models with shared parameters will still be defining everything over again. ### OpenAPI Generated typescript-fetch type definitions OpenAPI Generated's generated typescript-fetch SDK is much more verbose their either Speakeasy or Oazapfts. It does not seem too familiar with TypeScript and uses it rather loosely with a whole lot of if statements, and the bank account vs card payment logic really seems awkward. ```ts /** * A card (debit or credit) to take payment from. * @export * @interface Card */ export interface Card { /** * * @type {string} * @memberof Card */ object?: CardObjectEnum; /** * Cardholder's full name as it appears on the card. * @type {string} * @memberof Card */ name: string; /** * The card number, as a string without any separators. On read all but the last four digits will be masked for security. * @type {string} * @memberof Card */ number: string; /** * Card security code, 3 or 4 digits usually found on the back of the card. * @type {string} * @memberof Card */ cvc: string; /** * Two-digit number representing the card's expiration month. * @type {number} * @memberof Card */ expMonth: number; /** * Four-digit number representing the card's expiration year. * @type {number} * @memberof Card */ expYear: number; /** * * @type {string} * @memberof Card */ addressPostCode?: string; } /** * @export */ export const CardObjectEnum = { Card: 'card' } as const; export type CardObjectEnum = typeof CardObjectEnum[keyof typeof CardObjectEnum]; /** * Check if a given object implements the Card interface. */ export function instanceOfCard(value: object): value is Card { if (!('name' in value) || value['name'] === undefined) return false; if (!('number' in value) || value['number'] === undefined) return false; if (!('cvc' in value) || value['cvc'] === undefined) return false; if (!('expMonth' in value) || value['expMonth'] === undefined) return false; if (!('expYear' in value) || value['expYear'] === undefined) return false; return true; } export function CardFromJSON(json: any): Card { return CardFromJSONTyped(json, false); } export function CardFromJSONTyped(json: any, ignoreDiscriminator: boolean): Card { if (json == null) { return json; } return { 'object': json['object'] == null ? undefined : json['object'], 'name': json['name'], 'number': json['number'], 'cvc': json['cvc'], 'expMonth': json['exp_month'], 'expYear': json['exp_year'], 'addressPostCode': json['address_post_code'] == null ? undefined : json['address_post_code'], }; } export function CardToJSON(json: any): Card { return CardToJSONTyped(json, false); } export function CardToJSONTyped(value?: Card | null, ignoreDiscriminator: boolean = false): any { if (value == null) { return value; } return { 'object': value['object'], 'name': value['name'], 'number': value['number'], 'cvc': value['cvc'], 'exp_month': value['expMonth'], 'exp_year': value['expYear'], 'address_post_code': value['addressPostCode'], }; } ``` It's even managed to output some syntax errors and import some dependencies that were not used. Was it meant to use those imports somewhere, or is it bringing in unnecessary dependencies? Unclear. ![](/assets/docs/openapi-generator-ts-errors.png) ### OpenAPI Generator typescript-node type definitions Finally, how about this OpenAPI Generator typescript-node template? The typescript-node generator starts off looking simple enough, with a single type for any given payload: ```ts /** * A payment for a booking. */ export class BookingPayment { /** * Unique identifier for the payment. This will be a unique identifier for the payment, and is used to reference the payment in other objects. */ 'id'?: string; /** * Amount intended to be collected by this payment. A positive decimal figure describing the amount to be collected. */ 'amount'?: number; /** * Three-letter [ISO currency code](https://www.iso.org/iso-4217-currency-codes.html), in lowercase. */ 'currency'?: BookingPayment.CurrencyEnum; 'source'?: BookingPaymentSource; /** * The status of the payment, one of `pending`, `succeeded`, or `failed`. */ 'status'?: BookingPayment.StatusEnum; static discriminator: string | undefined = undefined; static attributeTypeMap: Array<{name: string, baseName: string, type: string}> = [ { "name": "id", "baseName": "id", "type": "string" }, { "name": "amount", "baseName": "amount", "type": "number" }, { "name": "currency", "baseName": "currency", "type": "BookingPayment.CurrencyEnum" }, { "name": "source", "baseName": "source", "type": "BookingPaymentSource" }, { "name": "status", "baseName": "status", "type": "BookingPayment.StatusEnum" } ]; static getAttributeTypeMap() { return BookingPayment.attributeTypeMap; } } export namespace BookingPayment { export enum CurrencyEnum { Bam = 'bam', Bgn = 'bgn', Chf = 'chf', Eur = 'eur', Gbp = 'gbp', Nok = 'nok', Sek = 'sek', Try = 'try' } export enum StatusEnum { Pending = 'pending', Succeeded = 'succeeded', Failed = 'failed' } } ``` It's hoisted some of the properties up into enums, and namespaced them which is nice. The polymorphic `source` property is defined as a separate type `BookingPaymentSource` in its own file, and here is how that looks: ```ts import { RequestFile } from './models'; import { BankAccount } from './bankAccount'; import { Card } from './card'; /** * The payment source to take the payment from. This can be a card or a bank account. Some of these properties will be hidden on read to protect PII leaking. */ export class BookingPaymentSource { 'object'?: BookingPaymentSource.ObjectEnum; 'name': string; /** * The account number for the bank account, in string form. Must be a current account. */ 'number': string; /** * Card security code, 3 or 4 digits usually found on the back of the card. */ 'cvc': string; /** * Two-digit number representing the card\'s expiration month. */ 'expMonth': number; /** * Four-digit number representing the card\'s expiration year. */ 'expYear': number; 'addressPostCode'?: string; /** * The sort code for the bank account, in string form. Must be a six-digit number. */ 'sortCode'?: string; /** * The name of the bank associated with the routing number. */ 'bankName': string; static discriminator: string | undefined = undefined; static attributeTypeMap: Array<{name: string, baseName: string, type: string}> = [ { "name": "object", "baseName": "object", "type": "BookingPaymentSource.ObjectEnum" }, { "name": "name", "baseName": "name", "type": "string" }, { "name": "number", "baseName": "number", "type": "string" }, { "name": "cvc", "baseName": "cvc", "type": "string" }, { "name": "expMonth", "baseName": "exp_month", "type": "number" }, { "name": "expYear", "baseName": "exp_year", "type": "number" }, { "name": "addressPostCode", "baseName": "address_post_code", "type": "string" }, { "name": "sortCode", "baseName": "sort_code", "type": "string" }, { "name": "bankName", "baseName": "bank_name", "type": "string" } ]; static getAttributeTypeMap() { return BookingPaymentSource.attributeTypeMap; } } export namespace BookingPaymentSource { export enum ObjectEnum { BankAccount = 'bank_account' } } ``` This is completely incorrect, as the `oneOf` for `Card` and `BankAccount` has been flattened into a single class with all the properties of both types. This means that when creating a `BookingPaymentSource` object, all properties from both `Card` and `BankAccount` are available, which is not the intended behavior at all. Some older generators require the optional `discriminator` property in the OpenAPI document to handle scenarios that a `oneOf` should otherwise handle by itself, but even adding that doesn't help here. ```yaml source: oneOf: - $ref: '#/components/schemas/Card' - $ref: '#/components/schemas/BankAccount' discriminator: propertyName: object ``` It still produces the exact same output. ### Type generation summary Here's a summary of how each generator handles OpenAPI polymorphism:
## Retries The SDK managed by Speakeasy can automatically retry failed network requests or retry requests based on specific error responses, providing a straightforward developer experience for an otherwise complicated topic. To enable this feature use the Speakeasy `x-speakeasy-retries` extension in the OpenAPI document. Here is an example updating `openapi.yaml` to add retries to the `create-booking` operation. ```yaml x-speakeasy-retries: strategy: backoff backoff: initialInterval: 500 # 500 milliseconds maxInterval: 60000 # 60 seconds maxElapsedTime: 3600000 # 5 minutes exponent: 1.5 ``` Add this snippet to the operation: ```yaml #... paths: /bookings: # ... post: #... operationId: create-booking x-speakeasy-retries: strategy: backoff backoff: initialInterval: 500 # 500 milliseconds maxInterval: 60000 # 60 seconds maxElapsedTime: 3600000 # 5 minutes exponent: 1.5 ``` Now we'll rerun the Speakeasy generator to enable retries, and the SDK will automatically attempt to retry failed network requests when booking a trip. It is also possible to enable retries for the SDK as a whole by adding a global `x-speakeasy-retries` at the root of the OpenAPI document instead of per operation. ## React Hooks [React Hooks](https://react.dev/reference/react/hooks) are a popular way to manage state and side effects in React applications. Speakeasy generates built-in React Hooks using [TanStack Query](https://tanstack.com/query/latest). These hooks provide features like intelligent caching, type safety, pagination, and seamless integration with modern React patterns such as SSR and Suspense. ```ts filename="example/booksView.tsx" import { useQuery } from "@tanstack/react-query"; function BookShelf() { // loads books from an API const { data, status, error } = useQuery([ "books" // Cache key for the query ], async () => { const response = await fetch("https://api.example.com/books"); return response.json(); }); if (status === "loading") return

Loading books...

; if (status === "error") return

Error: {error?.message}

; return (
    {data.map((book) => (
  • {book.title}
  • ))}
); } ``` For example, in this basic implementation, the `useQuery` hook fetches data from an API endpoint. The cache key ensures unique identification of the query. The `status` variable provides the current state of the query: `loading`, `error`, or `success`. Depending on the query status, the component renders `loading`, `error`, or the fetched data as a list. None of the other generators generate React Hooks for their SDKs.
For an in-depth look at how Speakeasy uses React Hooks, see our [official release article](https://www.speakeasy.com/post/release-react-hooks). ## Pagination SDKs managed by Speakeasy include optional [pagination for OpenAPI operations](/docs/customize/runtime/pagination). We'll update our pet store schema to add an `x-speakeasy-pagination` extension and a `page` query parameter: ```yaml paths: /stations: get: x-speakeasy-pagination: type: offsetLimit inputs: - name: page in: parameters type: page outputs: results: $ parameters: - name: page in: query description: The offset to start from required: false schema: type: integer default: 0 ``` After regenerating the SDK with Speakeasy, the `get-stations` operation is automatically paginated, and can be iterated through with async/await until the clients needs are met. ```ts import { TrainTravel } from "train-travel-sdk"; const trainTravel = new TrainTravel({ oAuth2: process.env["TRAINTRAVEL_O_AUTH2"] ?? "", }); async function run() { const result = await trainTravel.stations.get({ coordinates: "52.5200,13.4050", search: "Milano Centrale", country: "DE", }); for await (const page of result) { console.log(page); } } run(); ``` None of the other generators include pagination as a feature, leaving it all to the API client developers to figure out.
## Streaming files & data All the generators in our comparison generate SDKs that use the [Fetch API](https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API), which enables streaming for large uploads or downloads. Speakeasy makes this clear by providing documentation showing how to use streaming for file uploads in the README. It's important to show developer-users how to take advantage of this streaming, and helping them handle large file uploads in different runtimes will cut down on support interactions. ```typescript filename="example/train-travel-sdk/README.md" mark=7:13 import { TrainTravel } from "train-travel-sdk"; const trainTravel = new TrainTravel({ oAuth2: process.env["TRAINTRAVEL_O_AUTH2"] ?? "", }); async function run() { const result = await trainTravel.bookings.createRaw( bytesToStream( new TextEncoder().encode( "{\"trip_id\":\"4f4e4e1-c824-4d63-b37a-d8d698862f1d\",\"passenger_name\":\"John Doe\"}", ), ), ); console.log(result); } run(); ``` Beyond simply uploading and download files with streaming, Speakeasy SDKs also support JSON streaming for large JSON payloads using standards and conventions like [JSONL](https://jsonlines.org/) or [ND-JSON](https://ndjson.org/). This is particularly useful when dealing with large datasets that may not fit into memory all at once. Speakeasy provides built-in support for JSON streaming, allowing developers to process JSON data in chunks as it is received. ```typescript import { SDK } from '@speakeasy/sdk'; const sdk = new SDK(); async function streamLogs() { const result = await sdk.logs.fetch_logs(); for await (const event of result) { // Each event is a parsed JSON object from the stream console.log(`[${event.timestamp}] ${event.message}`); } } streamLogs().catch(error => { console.error('Error streaming logs:', error); }); ``` OpenAPI Generator and Oazapfts do not support JSON streaming in their generated SDKs, which limits their ability to handle large JSON payloads efficiently. There's an extra issue with OpenAPI Generator's typescript-node SDK, in that its content negotiation strategy (looking at `Accept` and `Content-Type` headers) is overly simplistic. It checks if the content type includes `application/json` with the line `if (produces.indexOf('application/json') >= 0) {` which is too broad, because `application/jsonl` will match that condition. If an API returns `application/jsonl` it will try to parse the response as a single JSON object instead of a stream of JSON objects, which will lead to runtime errors and frustrated developer-users.
## Generated documentation Of all the generators tested, Speakeasy was the only one to generate documentation and usage examples for SDKs. Speakeasy considers documentation generation as a crucial feature to enable rapid adoption and ease of use when an SDK can be published to NPM, and not something that should be left to the API team to produce from scratch.
Speakeasy generates a `README.md` generated at the root of the SDK, [which you can customize](/docs/sdk-docs/edit-readme) to add branding, support links, a code of conduct, and any other information your developer-users might find helpful. The Speakeasy SDK also includes working usage examples for all operations, complete with imports and appropriately formatted examples from the OpenAPI description. This is a huge help to developers getting started with the SDK, as they can copy and paste working code snippets directly into their applications. Here's an example of an operation from the Train Travel SDK's `README.md`: ```ts filename="example/train-travel-sdk/README.md" mark=8:12 import { TrainTravel } from "train-travel-sdk"; const trainTravel = new TrainTravel({ oAuth2: process.env["TRAINTRAVEL_O_AUTH2"] ?? "", }); async function run() { const result = await trainTravel.stations.get({ coordinates: "52.5200,13.4050", search: "Milano Centrale", country: "DE", }); for await (const page of result) { console.log(page); } } run(); ``` ## Bundling applications for the browser Speakeasy creates SDKs that are [tree-shakable](https://webpack.js.org/guides/tree-shaking/) and can be bundled for the browser using tools like Webpack, Rollup, or esbuild. Because Speakeasy supports a wider range of OpenAPI features, Speakeasy-created SDKs are likely to be slightly larger than those generated by other tools. Speakeasy also limits abstraction, which can lead to larger SDKs. This does not translate to a larger bundle size, as the SDK can be tree-shaken to remove unused code. Any SDK that supports runtime type checking or validation will have a larger bundle size, but the benefits of type checking and validation far outweigh the cost of a slightly larger bundle. If you use the popular validation library [Zod](https://zod.dev/) in your application already, you can exclude it from the SDK bundle to reduce its size. Here's an example of how to exclude Zod from the SDK bundle: ```bash filename="Terminal" mark=7 npx esbuild src/speakeasy-app.ts \ --bundle \ --minify \ --target=es2020 \ --platform=browser \ --outfile=dist/speakeasy-app.js \ --external:zod ``` ## A live example: Vessel API Node SDK [Vessel](https://www.vessel.dev/) trusts Speakeasy to generate and publish SDKs for its widely used APIs. We recently spoke to Zach Kirby about how Vessel uses Speakeasy. Zach shared that [the Vessel Node SDK](https://www.npmjs.com/package/@vesselapi/nodesdk) is downloaded from npm hundreds of times a week. ## Summary The open-source SDK generators we tested are all good and clearly took tremendous effort and community coordination to build and maintain. Different applications have widely differing needs, and smaller projects may not need all the features offered by Speakeasy. If you are building an API that developers rely on and would like to publish full-featured SDKs that follow best practices, we strongly recommend giving the [Speakeasy SDK generator](/docs/speakeasy-reference/cli/getting-started) a try. [Join our Slack community](https://go.speakeasy.com/slack) to let us know how we can improve our TypeScript SDK generator or to suggest features. # Standalone Functions Source: https://speakeasy.com/docs/sdks/languages/typescript/standalone-functions import { Callout } from "@/mdx/components"; ## Feature Overview Every method in TypeScript SDKs generated by Speakeasy is also available as a standalone function. This alternative API is ideal for browser or serverless environments, where bundlers can optimize applications by tree-shaking unused functionality. This includes unused methods, Zod schemas, encoding helpers, and response handlers. As a result, the application's final bundle size is dramatically smaller and grows very gradually as more of the generated SDK is used. Using methods through the main SDK class remains a valid and generally more ergonomic option. Standalone functions are an optimization designed for specific types of applications. ## Usage **Step 1: Import the Core Class and Function** First, import the `Core` SDK class for authentication and setup, along with the required standalone function. An SDK named `Todo`, for example, might look like this: ```typescript filename="index.ts" import { TodoCore } from "todo/core.js"; import { todosCreate } from "todo/funcs/todosCreate.js"; ``` The `Core` SDK class is optimized for tree-shaking, and can be reused throughout the application. **Step 2: Instantiate the Core Class** Create an instance of the `Core` class with the required configuration (e.g., an API Key): ```typescript filename="index.ts" const todoSDK = new TodoCore({ apiKey: "TODO_API_KEY", }); ``` **Step 3: Call the Standalone Function & Handle the Result** Invoke the standalone function, passing the core instance the first parameter. Handle the result using a switch statement for comprehensive error handling: ```typescript filename="index.ts" async function run() { const res = await todosCreate(todoSDK); switch (true) { case res.ok: // Successful response is processed later. break; case res.error instanceof SDKValidationError: // Display validation errors in a readable format. return console.log(res.error.pretty()); case res.error instanceof Error: // Handle general errors. return console.log(res.error); default: // Ensure all error cases are exhaustively handled. res.error satisfies never; throw new Error("Unexpected error case: " + res.error); } const { value: todo } = res; // Handle the successful result. console.log(todo); } run(); ``` ## Result Types Standalone functions differ from SDK methods in that they return a `Result` type to capture _known errors_ and document them through the type system. This approach avoids throwing errors, allowing application code to maintain clear control flow while making error handling a natural part of the application code. The term **"known errors"** is used because standalone functions and JavaScript code can still throw unexpected errors (e.g., `TypeError`, `RangeError`, and `DOMException`). While exhaustively catching all errors may be addressed in future SDK versions, there's significant value in capturing most errors and converting them into values. Another reason for this programming style is that these functions are commonly used in front-end applications where throwing exceptions is often discouraged. React and similar frameworks promote this approach to ensure components can render appropriate content in all states—loading, success, and error. Thus, the general pattern when calling standalone functions looks like this: ```typescript filename="log-something.ts" import { Core } from ""; import { fetchSomething } from "/funcs/fetchSomething.js"; const client = new Core(); async function run() { const result = await fetchSomething(client, { id: "123" }); if (!result.ok) { // Either throw the error or handle it based on application requirements. throw result.error; } console.log(result.value); } run(); ``` Note that, unlike a try-catch block where errors are of type `unknown` (or `any` depending on TypeScript settings), `result.error` in this example has a specific, explicit type. # Create Unity SDKs from OpenAPI / Swagger Source: https://speakeasy.com/docs/sdks/languages/unity/methodology-unity import { Callout } from "@/mdx/components"; ## Unity SDK Overview The Speakeasy Unity C# SDK supports Unity 2021.3 LTS and above and is designed to be strongly typed, light on external dependencies, easy to debug, and easy to use. Some of the core features of the SDK include: - Interfaces for core components allow for dependency injection and mocking. - Generated C# doc comments to enhance the SDK's IntelliSense compatibility and developer experience. - Async/await support for all API calls, which can easily be wrapped in coroutines if needed. - Optional pagination support for supported APIs. - Support for complex number types: - `System.Numbers.BigInteger` - `System.Decimal` - Support for both string- and integer-based enums. - Streaming downloads for files. The SDK includes minimal dependencies. The only external dependencies are: - `newtonsoft.json` for JSON serialization and deserialization. - The UnityEngine libraries. ## Unity Package Structure ```yaml filename="lib-structure.yaml" ├── {SDK Class Name} # The root namespace for the SDK where {SDK Class Name} is the provided name of the SDK | ├── {SDK Class Name}.csproj | ├── {SDK Class Name}SDK.cs # The main SDK class | ├── ... # Other SDK classes | ├── Models # The namespace for the SDK's models | | ├── Operations # The namespace for the SDK's operations models which generally house the request/response models for each API | | | ├── ... | | └── Shared # The namespace for the SDK's models generated from components in the OpenAPI document | | ├── ... | └── Utils # The namespace for the SDK's utility classes ├── docs # Markdown files for the SDK's documentation | └── ... ├── {SDK Class Name}.sln # The SDK's solution file └── ... ``` ## HTTP Client The Unity C# SDK provides an interface for the HTTP client used to make API calls. A custom HTTP client can be provided to the SDK as long as it conforms to the interface. ```csharp public interface ISpeakeasyHttpClient { void AddHeader(string key, string value); void AddQueryParam(string key, string value); Task SendAsync(UnityWebRequest message); } ``` By default, the SDK will instantiate its own client using `UnityWebRequest.SendWebRequest()` to send the request, but this can be overridden by providing a custom implementation of the `ISpeakeasyHttpClient` interface: ```csharp var client = new CustomHttpClient(); var sdkInstance = new SDK(client); ``` This is useful if you're using a custom HTTP client that supports `UnityWebRequests` and a proxy or other custom configuration, or to provide a client preconfigured with standard headers. ## Data Types and Classes The C# SDK uses as many native types from the standard library as possible, for example: - `string` - `System.DateTime` - `int` - `long` - `System.Numberics.BigInteger` - `float` - `double` - `decimal` - `bool` The SDK will only fall back on custom types when the native types are not suitable, for example: - A custom `DateOnly` class for `date` types - Custom `enum` types for `string` and `integer` based enums Speakeasy will generate standard C# classes with public fields that use attributes to guide the serialization and deserialization processes. The classes are also `Serializable`, with `[SerializeField]` attributes on the fields allowing them to be used in the Unity Inspector. ## Parameters If parameters are defined in the OpenAPI document, Speakeasy will generate methods with parameters. The number of parameters defined should not exceed the `maxMethodParams` value configured in the `gen.yaml` file. If they do or the `maxMethodParams` value is set to `0`, Speakeasy will generate a request object that allows for all parameters to be passed in a single object. ## Async Support The Unity C# SDK is generated with async/await support for all API calls, which can easily be wrapped in coroutines if needed. For example: ```csharp using System; using System.Collections.Generic; using System.Collections; using System.IO; using System.Threading.Tasks; // Static methods that help using the SDK in Unity coroutines public static class CoroutineHelper { public static IEnumerator Await(Task task) { while (!task.IsCompleted) { yield return null; } if (task.IsFaulted) { throw task.Exception; } } public static IEnumerator Await(Func taskDelegate) { return Await(taskDelegate.Invoke()); } } ``` The example above can be used like so: ```csharp yield return CoroutineHelper.Await(async () => { var sdk = new SDK(); using ( var res = await sdk.SomeMethod(...) ) { // Handle response } }); ``` Due to the nature of the underlying `UnityWebRequest`, the response is an `IDisposable` object that should be disposed of when finished with or used within a `using` statement as shown above. ## Errors The Unity C# SDK will throw exceptions for network or invalid request errors. For unsuccessful responses, the SDK will return a response object containing the status code and response body, which can be checked for the status of the method call. Support for throwing unsuccessful status codes as exceptions is coming soon. ## User Agent Strings The Unity SDK will include a [user agent](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/User-Agent) string in all requests that can be leveraged for tracking SDK usage amongst broader API usage. The format is as follows: ```stmpl speakeasy-sdk/unity {{SDKVersion}} {{GenVersion}} {{DocVersion}} {{PackageName}} ``` Where - `SDKVersion` is the version of the SDK, defined in `gen.yaml` and released - `GenVersion` is the version of the Speakeasy generator - `DocVersion` is the version of the OpenAPI document - `PackageName` is the name of the package defined in `gen.yaml` # Handling breaking changes in SDKs Source: https://speakeasy.com/docs/sdks/manage/breaking-changes import { Callout } from "@/mdx/components"; This guide explains how to handle breaking changes in APIs when using Speakeasy-generated SDKs. Follow these guidelines to maintain backward compatibility and ensure a smooth experience for your SDK users. ## Safe changes The following API changes are safe and won't break existing SDKs: ### Adding new fields You can safely add new fields to API responses because older SDK versions ignore these fields. Adding new response fields doesn't break existing integrations. ### Adding new enum values You can safely add new enum values when enums are marked with `x-speakeasy-unknown-values` (see our [Customize enums](/docs/customize/data-model/enums#open-enums) documentation). Older SDKs handle these new values gracefully according to the behavior specified in the extension configuration. ## Changes requiring caution Some API changes require careful consideration to avoid breaking existing SDK implementations. Use the [OpenAPI diff tool](/docs/speakeasy-reference/cli/openapi/diff) to identify potential breaking changes in your API specification. ### Deprecating required fields Take care when deprecating fields marked as required in the API specification. Older SDKs may throw validation errors if required fields are missing: - Make the field optional before removing it. - Plan a deprecation period for implementation updates. ### Modifying oneOf schemas Make changes to `oneOf` schemas carefully, as adding new variants may cause type mismatch errors in older SDKs: - Maintain backward compatibility with existing schema variants. - Test changes thoroughly with older SDK versions. ## Future improvements Speakeasy is developing additional features to help you manage breaking changes, including: - SDK version upgrade prompts - Improved tooling for breaking changes - Enhanced version management capabilities For more information about SDK versioning and how Speakeasy handles version bumps, see our [SDK versioning guide](./versioning). # Forward compatibility Source: https://speakeasy.com/docs/sdks/manage/forward-compatibility import { Callout, CodeWithTabs, Table } from "@/mdx/components"; This guide explains how Speakeasy-generated SDKs maintain forward compatibility when APIs evolve. Forward compatibility ensures older SDK versions continue to work correctly when the API adds new fields, enum values, or other data. ## Forward compatibility at a glance Forward compatibility ensures older SDK versions continue to work when APIs evolve. The table below shows which changes are safe and which require special handling.
## Common forward compatibility scenarios This section covers the most frequent scenarios encountered when evolving APIs and how Speakeasy handles them to maintain forward compatibility. ### Handling new fields Adding new fields to API responses is safe because older SDK versions ignore these fields. When an API response includes fields not defined in the SDK's model, these fields are simply not deserialized. Older SDK versions will continue to work without errors, ignoring the `updated_at` field. This allows APIs to evolve by adding new data without breaking existing integrations. ### Handling new enum values APIs often need to add new enum values over time. Speakeasy provides the `x-speakeasy-unknown-values` extension to handle this gracefully. ```yaml status: type: string x-speakeasy-unknown-values: allow enum: - active - inactive - pending ``` When the API adds a new enum value (e.g., `suspended`), older SDK versions handle it according to the language: - **TypeScript** - **Python** - **Go** - **Java** This prevents runtime errors when new enum values are encountered, allowing APIs to add new states without breaking existing clients. ### Handling unexpected data Speakeasy-generated SDKs include built-in mechanisms to handle unexpected data: 1. **Validation errors**: SDKs provide detailed validation errors when unexpected data is received, making debugging easier 2. **OneOf schemas**: When using `oneOf` schemas, SDKs can handle evolving data structures by attempting to match against known variants 3. **Optional fields**: Fields marked as optional in the OpenAPI spec won't cause validation errors if missing ### Handling unexpected response codes APIs evolve over time and may introduce new response codes. Speakeasy-generated SDKs are designed to handle unexpected response codes gracefully: ```yaml responses: "2xx": description: Success response content: application/json: schema: $ref: "#/components/schemas/SuccessResponse" "4xx": description: Error response content: application/json: schema: $ref: "#/components/schemas/ErrorResponse" ``` #### Benefits of status code ranges 1. **Flexible status codes**: Using `2xx` and `4xx` patterns allows APIs to add new specific status codes (like `201` or `429`) without breaking existing SDKs 2. **Consistent error handling**: All error responses follow the same structure, making it easier to handle new error types 3. **Graceful degradation**: Even when encountering unexpected status codes, SDKs can still extract useful information from the response When an API returns a status code that wasn't explicitly defined in the original specification, Speakeasy SDKs: - Match it to the appropriate range (`2xx`, `4xx`, `5xx`) - Parse the response using the defined schema for that range - Provide access to both the status code and response body ## Advanced forward compatibility techniques These advanced techniques help maintain forward compatibility in more complex scenarios. ### Deprecating fields When evolving APIs, deprecating fields is a common necessity. Speakeasy provides extensions to handle field deprecation gracefully while maintaining forward compatibility: ```yaml properties: name: type: string sku: type: string deprecated: true x-speakeasy-deprecation-message: We no longer support the SKU property. ``` #### Benefits of proper deprecation 1. Fields remain accessible to older SDK versions 2. New SDK versions mark these fields with proper deprecation annotations 3. Generated documentation includes deprecation notices 4. Developers receive clear guidance on migration #### Field removal process When planning to remove a field entirely: 1. Mark the field as optional first 2. Add deprecation notices with the `deprecated` keyword and `x-speakeasy-deprecation-message` 3. Allow sufficient time for users to update implementations 4. Remove the field only after a suitable deprecation period ### Forward-compatible unions To create forward-compatible unions that can handle new data types added in the future, use the oneOf pattern with a string fallback: ```yaml oneOf: - { type: "dog" } - { type: "cat" } - { type: string } ``` #### Benefits of string fallback 1. Provides strongly typed handling for known variants (`dog` and `cat` types) 2. Gracefully captures any future variants as string values 3. Prevents runtime errors when new variants are introduced 4. Allows SDK users to handle unknown variants safely Each language handles these unions differently: - **TypeScript**: Uses native union types with string fallback - **Python**: Leverages `typing.Union` with string fallback - **Go**: Generates helper methods for both known and unknown types - **Java**: Provides type discrimination with generic string handling ## TypeScript-specific configuration TypeScript SDKs have additional generator options that enable forward compatibility and fault tolerance by default. These options work together to ensure your SDK gracefully handles API evolution. ```yaml typescript: forwardCompatibleEnumsByDefault: true forwardCompatibleUnionsByDefault: tagged-only laxMode: lax unionStrategy: populated-fields ``` ### Forward-compatible enums When `forwardCompatibleEnumsByDefault` is enabled (the default for new TypeScript SDKs), enums used in responses accept unknown values instead of rejecting the response. Unknown values are captured in a type-safe `Unrecognized` wrapper: ```typescript const notification = await sdk.notifications.get(id); // Before: Error: Expected 'email' | 'sms' | 'push' // After: 'email' | 'sms' | 'push' | Unrecognized ``` ### Forward-compatible unions When `forwardCompatibleUnionsByDefault` is enabled (the default for new TypeScript SDKs), discriminated unions accept unknown values. Unknown variants are captured with their raw data and accessible via the `UNKNOWN` discriminator value: ```typescript const account = await sdk.accounts.getLinkedAccount(); // Before: Error: Unable to deserialize into any union member // After: // | { type: "email"; email: string } // | { type: "google"; googleId: string } // | { type: "UNKNOWN"; raw: unknown } ``` ### Lax mode When `laxMode` is set to `lax` (the default for new TypeScript SDKs), the SDK gracefully handles missing or mistyped fields in responses by applying zero-value defaults and type coercions. This ensures one missing field doesn't break the entire response: - Missing required strings become `""` - Missing required numbers become `0` - Missing required booleans become `false` - String `"true"` and `"false"` are coerced to booleans - Numeric strings are coerced to numbers Lax mode only affects response deserialization and never lies about types. The SDK types remain accurate. ### Smart union deserialization When `unionStrategy` is set to `populated-fields` (the default for new TypeScript SDKs), the SDK uses a smarter algorithm to pick the best union variant. Instead of trying each type in order and returning the first valid match, it tries all types and returns the one with the most matching fields. This prevents issues where one union variant is a subset of another and the wrong variant gets selected due to ordering. Individual enums and unions can override the global defaults using `x-speakeasy-unknown-values: allow` or `x-speakeasy-unknown-values: disallow` in your OpenAPI spec. See the [TypeScript configuration reference](/docs/speakeasy-reference/generation/ts-config#forward-compatibility) for all available options. ## Guard-rails for breaking changes Speakeasy provides several tools to detect and prevent breaking changes: ### Version your API Create a versioning strategy for your API to manage breaking changes: - Use path-based versioning (e.g., `/v1/resource`, `/v2/resource`) - Include version in request headers (`Api-Version: 2023-01-01`) - Maintain multiple API versions simultaneously during migration periods ### Add defaults for optional fields When making required fields optional: - Always include default values to maintain backward compatibility - Document the default behavior clearly - Use the `default` property in your OpenAPI specification: ```yaml properties: status: type: string default: "active" ``` ### Open your enums Convert closed enums to open enums using the Speakeasy extension: ```yaml status: type: string x-speakeasy-unknown-values: allow enum: - active - inactive - pending ``` ### Use the OpenAPI diff tool The [OpenAPI diff tool](/docs/speakeasy-reference/cli/openapi/diff) identifies potential breaking changes between API specification versions: ```bash speakeasy openapi diff --base v1.yaml --revision v2.yaml ``` This highlights changes that might break backward compatibility, such as: - Removing required fields - Changing field types - Modifying oneOf schemas ### SDK version management Speakeasy automatically manages SDK versioning based on the nature of changes: - Patch version for non-breaking changes - Minor version for backward-compatible additions - Major version for breaking changes ### Breaking change notifications When generating SDKs, Speakeasy detects breaking changes and provides clear notifications about what changed and how to handle the transition. For more information about handling breaking changes, see the [breaking changes guide](./breaking-changes). # Set up SDK on GitHub Source: https://speakeasy.com/docs/sdks/manage/github-setup import { Callout, Screenshot } from "@/mdx/components"; The [Speakeasy CLI](https://github.com/speakeasy-api/speakeasy) and the Speakeasy [SDK Generation GitHub Action](https://github.com/speakeasy-api/sdk-generation-action) power the SDK generation workflow. The workflow automates the process of: - Downloading or loading the OpenAPI document from a URL or repository. - Validating the OpenAPI document. - Generating SDKs for multiple languages. - Committing the generated SDKs to the repository or opening a pull request (PR). Branches and pull requests created by the GitHub action are owned by it. Any future generations (whether scheduled or manually triggered) will overwrite all changes in that branch/PR. To make changes to the generated code, create a separate PR instead of modifying the action-generated PR. ## Example workflow file ```yml name: SDK Generation permissions: checks: write contents: write pull-requests: write statuses: write on: workflow_dispatch: inputs: force: description: Force SDK generation, even if no changes are detected. type: boolean default: false runs-on: description: Runner to use for the workflow (e.g., large-ubuntu-runner) type: string default: ubuntu-latest jobs: generate: uses: speakeasy-api/sdk-generation-action/.github/workflows/workflow-executor.yaml@v15 with: speakeasy_version: latest force: ${{ github.event.inputs.force }} mode: pr runs-on: ${{ github.event.inputs.runs-on }} secrets: github_access_token: ${{ secrets.GITHUB_TOKEN }} speakeasy_api_key: ${{ secrets.SPEAKEASY_API_KEY }} ``` ## Step-by-step guide ### Initialize the SDK repository Create a new GitHub repository to host the autogenerated SDKs. It is recommended to use a separate repository for each SDK, but a [monorepo](/guides/sdks/creating-a-monorepo) is also supported. ### Generate the SDK workflow configuration Run the Speakeasy CLI to configure the SDK generation workflow. This command creates the necessary workflow files. ```bash speakeasy configure github ``` After running the command, `.speakeasy/workflow.yaml` and `.github/workflows/sdk_generation.yaml` will be created. These files define the SDK generation workflow and the associated GitHub Action. If further customization is needed, [manual configuration](/docs/workflow-reference/generation-reference) of the workflow files is available. ### Set up GitHub secrets Configure GitHub secrets for authentication: - Navigate to **Settings > Secrets & Variables > Actions** in your GitHub repository. - Add a new secret named `SPEAKEASY_API_KEY` which can be obtained from the Speakeasy dashboard. ### Push the workflow to GitHub Commit and push the generated workflow files to the repository. To test the GitHub Action without modifying any GitHub branches, put the action into `mode: test`. Navigate to **Actions** in the GitHub repository to trigger the SDK generation workflow manually or wait for it to run automatically. A green checkmark indicates successful workflow completion. If the publishing step has not been configured, it will be skipped. For details on package publishing, refer to the [Publishing SDKs guide](/docs/publish-sdk). ### Update the GitHub Actions workflow permissions If the error `403 GitHub Actions is not permitted to create or approve pull requests` occurs, the repository's GitHub Actions permissions must be updated. Navigate to **Settings > Actions > Workflow permissions** and adjust the permissions accordingly. ### Configure remote URLs for schemas Remote URLs for OpenAPI schemas must remain stable. Dynamically constructed URLs in workflow files are not supported. If the OpenAPI schema is hosted in another repository or at a remote URL, set the `source` as the remote URL in `.speakeasy/workflow.yaml`. Use the following command to add the remote URL: If the remote URL requires authentication, follow the prompts to provide a token or key stored as an environment variable (for example, `$OPENAPI_DOC_AUTH_TOKEN`). **Important**: When fetching OpenAPI documents from private repositories, ensure that you prefix the token value with `Bearer ` when setting the value. For example: ```bash OPENAPI_DOC_AUTH_TOKEN="Bearer " ``` Add a [GitHub secret](https://docs.github.com/en/actions/security-guides/using-secrets-in-github-actions) with the same name and value as the token or key (including the `Bearer ` prefix for private repositories). ## Using larger GitHub-hosted runners For resource-intensive SDK generation builds (such as large TypeScript projects that fail during compilation due to memory constraints), you can configure larger GitHub-hosted runners with more RAM and CPU resources. ### Set up larger runners 1. **Configure larger runners in your GitHub organization**: Larger runners are not enabled by default and require separate billing setup. - Follow the [GitHub documentation for managing larger runners](https://docs.github.com/en/enterprise-cloud@latest/actions/how-tos/using-github-hosted-runners/using-larger-runners/managing-larger-runners) - Choose a runner size with sufficient RAM (standard `ubuntu-latest` runners have only 7GB RAM) - Assign a custom label to your larger runner (e.g., `large-ubuntu-runner`) 2. **Update your workflow**: Once your larger runner is configured, update your workflow file to use the custom runner label: ```yml name: SDK Generation permissions: checks: write contents: write pull-requests: write statuses: write on: workflow_dispatch: inputs: force: description: Force SDK generation, even if no changes are detected. type: boolean default: false runs-on: description: Runner to use for the workflow (e.g., large-ubuntu-runner) type: string default: large-ubuntu-runner # Use your custom label here jobs: generate: uses: speakeasy-api/sdk-generation-action/.github/workflows/workflow-executor.yaml@v15 with: speakeasy_version: latest force: ${{ github.event.inputs.force }} mode: pr runs-on: ${{ github.event.inputs.runs-on }} secrets: github_access_token: ${{ secrets.GITHUB_TOKEN }} speakeasy_api_key: ${{ secrets.SPEAKEASY_API_KEY }} ``` This configuration allows you to specify different runner types when manually triggering the workflow, or set a default larger runner for all builds. ## Enable signed commits To include signed commits from the SDK Generation workflow, add `signed_commits: true` to the workflow file. This configuration ensures that GitHub Actions creates verified commits during workflow execution. ```yml name: SDK Generation permissions: checks: write contents: write pull-requests: write statuses: write on: workflow_dispatch: inputs: force: description: Force SDK generation, even if no changes are detected. type: boolean default: false runs-on: description: Runner to use for the workflow (e.g., large-ubuntu-runner) type: string default: ubuntu-latest jobs: generate: uses: speakeasy-api/sdk-generation-action/.github/workflows/workflow-executor.yaml@v15 with: speakeasy_version: latest force: ${{ github.event.inputs.force }} mode: pr runs-on: ${{ github.event.inputs.runs-on }} signed_commits: true secrets: github_access_token: ${{ secrets.GITHUB_TOKEN }} speakeasy_api_key: ${{ secrets.SPEAKEASY_API_KEY }} ``` # Poetry 2.0 and Python 3.9 updates Source: https://speakeasy.com/docs/sdks/manage/migrate/poetry-2-update This guide covers important updates to the Python SDK generation process related to Poetry 2.0 and Python 3.9 requirements. ## Changes overview To evolve with the Python ecosystem and maintain compatibility with current tooling, the Python SDK generation process implements two significant changes: 1. The minimum Python version for generated Python SDKs changes from 3.8 to 3.9. 2. Poetry packaging tool updates to version 2.0.0. ## Python 3.9 requirement Python 3.8 reached end-of-life (EOL) status in October 2024 and the Python language maintainers no longer support it for security and bug fixes. The last bug fix update occurred in May 2021, and the last security update occurred in September 2024. The key type safety tools used in the Speakeasy generation process have begun removing Python 3.8 support: - Poetry ([change](https://github.com/python-poetry/poetry/pull/9692)) - Mypy ([change](https://github.com/python/mypy/pull/17492)) - Pylint ([change](https://github.com/pylint-dev/pylint/pull/9774)) ## Poetry 2.0 update The Poetry version 2.0.0 release introduces breaking changes to CLI commands used in the generation process, affecting compatibility with version 1.x. By default, Poetry is installed at the latest version when following the documented installation options. ## Local development updates API producers running generation locally need to update Poetry to the new major version. The update process depends on the installation method: ```bash # If installed via pipx pipx upgrade poetry # If installed via the official installer poetry self update ``` If Poetry is not updated, the generator will output a dependency version error: ``` WARN can't compile - Dependency Version Mismatch - Install Poetry by following the instructions at https://python-poetry.org/docs/#installing-with-pipx. ERROR dependency version not met -- poetry - version 1.8.5 is less than required version 2.0.0 FATAL Failed to generate SDK to XXX ``` ## GitHub Actions configuration API producers using GitHub Actions (`speakeasy-api/sdk-generation-action@v15`) with the latest Speakeasy version will have Poetry automatically updated. No changes are needed for the most common usage. If the Speakeasy version is pinned to an older version, add the `poetry_version` input to the repository's workflow configuration to pin Poetry to the older major version: ```yaml uses: speakeasy-api/sdk-generation-action@v15 with: # ... other existing inputs ... poetry_version: "1.8.5" ``` ## Version update considerations Consistent with industry practice, the Speakeasy generator will suggest a minor version update for the generated SDK to reflect these Python changes. However, consider releasing the SDK as a major version update if API consumers are likely to be using Python 3.8, which was previously common for some data management SaaS platforms. For questions, reach out on [Slack](https://go.speakeasy.com/slack). # Upgrade to the Python v2 SDK with the Speakeasy CLI Source: https://speakeasy.com/docs/sdks/manage/migrate/python-migration To upgrade the Python SDK to Python v2 using the Speakeasy CLI, follow these four steps: ## Update the `gen.yaml` file - Add `templateVersion: v2` to the Python section of your `gen.yaml` file. - If you have an `additionalDependencies` section under `python`, it needs modification. If you haven't changed it previously, you can delete it and it will be recreated in the correct format. Otherwise, modify it from this: ```yaml additionalDependencies: dependencies: {} extraDependencies: dev: {} ``` To this: ```yaml additionalDependencies: dev: {} main: {} ``` - Move any dependencies listed under the `dependencies` key to `main`. - Move any dependencies under `extraDependencies.dev` to `dev`. - Move any additional keys under `extraDependencies` to the top-level `additionalDependencies` next to the other keys. ## Update the `author` key - Change the old `author` key, under `python`, to the new `authors` key, which is an array of authors. ```yaml python: authors: - Speakeasy # other configurations... ``` ## Generate the Python v2 SDK - Run the `speakeasy run` command to generate the Python SDK. ## Adjust the imports for Python v2 One of the main changes in Python v2 is how imported packages are handled. In version 1, `sdkClassName` specified the top-level module. In version 2, `packageName` is now used for imports, and matches the expected naming conventions for packages installed from PyPI. Some code imports may need to be adjusted for this change. For example, given the `sdkClassName` of `speakeasy` and the `packageName` of `speakeasy-sdk`, the code generation previously occurred in the `src/speakeasy` directory with imports as follows: ```python from speakeasy import Speakeasy ``` In version 2, the code is generated in the `src/speakeasy_sdk` directory and imported as follows: ```python from speakeasy_sdk import Speakeasy ``` - If you have custom hooks, move the custom hook files and the hook registration logic in `registration.py` from the old `sdkClassName`-based code-generation directory to the new `packageName`-based code-generation directory and update the imports accordingly: ```python # Old import from speakeasy.hooks import CustomHook # New import from speakeasy_sdk.hooks import CustomHook ``` - If you do not have custom hooks, delete the old `src/speakeasy` folder: ```sh rm -rf src/speakeasy ``` Feel free to reach out if you encounter any issues or need further assistance! # Migrate from Python poetry to uv Source: https://speakeasy.com/docs/sdks/manage/migrate/python-uv-update The Speakeasy Python SDK generator now uses `uv` as the default packaging and dependency management tool for generated SDKs. ## Changes overview To improve performance and align with modern Python tooling standards, the Python SDK generation process implements a significant change in dependency management: The default packaging and dependency management tool changes from `poetry` to `uv` for all newly generated SDKs. ## Why uv? Designed as a faster, more modern replacement for `poetry`, `uv` provides strong support for Python packaging standards and significantly improved performance for dependency resolution and installation. ## Continued poetry support Speakeasy continues to support `poetry`, and SDK producers who prefer it can opt in via generator configuration. ## What to expect - Newly generated SDKs default to `uv` unless overridden. - Existing workflows using `poetry` will continue to work without changes. - Future updates will focus on enhancing `uv` support as it becomes the ecosystem standard. For questions or migration help, reach out on [Slack](https://go.speakeasy.com/slack). # Migrate to Speakeasy workflows Source: https://speakeasy.com/docs/sdks/manage/migrate/workflow-migration The [`workflow.yaml` file](/docs/speakeasy-reference/workflow-file) allows defining all the relevant pieces of SDK generation as consistent configs in a single place. This new generation system is entirely portable. Generate SDKs locally, from GitHub, or from any CI system with absolute consistency. ## Who needs to migrate? Customers who started with Speakeasy in March 2024 or later are likely already using Speakeasy workflows. Verify this by checking the folders in the generated SDK. If the SDK contains a `.speakeasy` folder, it is already using workflows. If you started using Speakeasy before March 2024, follow the steps below to migrate to workflows. ## Migration steps For existing Speakeasy SDKs in GitHub, an easy migration tool is available. Navigate to the root of the repo and run the following command: ```bash speakeasy migrate ``` The following changes will appear as a result: - `.speakeasy/workflow.yaml` [NEW] - `.github/workflows/sdk_generation_action.yaml` [UPDATED] Push these changes up and you are ready to go! The produced files should look something like the following: ```yaml filename=".github/workflows/sdk_generation_action.yaml" name: Generate permissions: checks: write contents: write pull-requests: write statuses: write "on": workflow_dispatch: inputs: force: description: Force generation of SDKs type: boolean default: false schedule: - cron: 0 0 * * * jobs: generate: uses: speakeasy-api/sdk-generation-action/.github/workflows/workflow-executor.yaml@v15 with: force: ${{ github.event.inputs.force }} mode: pr secrets: github_access_token: ${{ secrets.GITHUB_TOKEN }} speakeasy_api_key: ${{ secrets.SPEAKEASY_API_KEY }} ``` ```yaml filename=".speakeasy/workflow.yaml" workflowVersion: 1.0.0 speakeasyVersion: latest sources: openapi: inputs: - location: https://raw.githubusercontent.com/speakeasy-sdks/template-speakeasy-bar/main/openapi.yaml targets: my-target: target: typescript source: openapi ``` It is now possible to generate the SDK locally using `speakeasy run`, while GitHub Actions automated generations will continue working as before. Any custom code in the current GitHub Actions workflow should automatically be moved over by the migration script. If you have any issues migrating to workflows, reach out to the Speakeasy team on Slack and we will help you resolve it quickly. ## Creating a new SDK repo To migrate by creating a new SDK repo, an easy-to-use command is available for setting up an SDK for the preferred specification and language. Create a new repo and run the following commands from the root directory: ```bash speakeasy quickstart speakeasy configure github ``` After creating the repo, copy the `gen.yaml` from the old SDK into `.speakeasy/gen.yaml` in the new SDK repo. The SDK is now ready to generate! # SDK changelogs Source: https://speakeasy.com/docs/sdks/manage/sdk-changelogs import { Screenshot } from "@/mdx/components"; ## Overview SDK changes should be transparent and easy to validate. Comprehensive SDK release notes serve two critical purposes: - Providing SDK maintainers with detailed summaries of every pull request helps them validate changes. - Providing end users with clear documentation allows them to track SDK evolution over time. Speakeasy automatically detects SDK changes and generates enhanced release notes. SDK maintainers can view these notes in commit messages and pull request (PR) descriptions. SDK users can view them in the public release notes and the commit history. ## Prerequisites You don't need to make configuration changes to enable the release notes feature. It works automatically with existing Speakeasy workflows when the following requirements are met: - The [sdk-generation-action](https://github.com/speakeasy-api/sdk-generation-action) v15.49.1 or above is enabled in GitHub Actions. - The [Speakeasy CLI](https://github.com/speakeasy-api/speakeasy) v1.605.5 or above is installed on your machine. ## PR summaries and commit messages for SDK maintainers Every SDK generation results in a commit message that includes a comprehensive summary of changes. These commit messages are bundled into a PR summary that allows SDK maintainers to validate SDK changes easily before merging them. The commit message includes a line for each change made to the SDK, including: - **Added methods:** New functionality being introduced - **Removed methods:** Deprecated functionality being removed (flagged as breaking) - **Modified methods:** Changed signatures, parameters, or behaviors - **Breaking change indicators:** Clear warnings for any backward-incompatible changes The following screenshot shows an example of how the commit messages and PR summaries appear in GitHub, with detailed information about method changes and breaking change indicators: This improvement helps SDK maintainers with code review and maintaining Git history hygiene. ## Detailed release notes for SDK users SDK users can check changes from two sources: - **The SDK commit history** shows when certain capabilities were added or modified. - **The public release notes** published with each version of the SDK show the changes made in that version. Once changes have been validated and merged by the maintainers, the following detailed information becomes part of the SDK's public release notes: - **Method-level change tracking:** A record of the methods that were added, modified, or removed - **Breaking change visibility:** Clear indicators for any changes that could impact existing integrations - **Version comparison:** Easy-to-scan summaries that help uses assess upgrade impact ### Modifying release notes As an SDK maintainer, you can add custom messaging to SDK release notes as follows: - Open the **PR** that requires the release notes changes. - Edit the `releaseNotes` section in the `gen.lock` file to add or remove information, as shown in the following screenshot: - **Commit** the change. Once you've merged the **PR** and a **GitHub release** has been generated, the entire text in the `ReleaseNotes` field in `gen.lock` is added to the **release notes body**, as shown in the following screenshot: # SDK Sandboxes Source: https://speakeasy.com/docs/sdks/manage/sdk-sandbox import { Callout } from "@/mdx/components"; Speakeasy can generate dev container configurations for SDK repositories to provide an intuitive, predefined sandbox environment to explore SDK capabilities. Beyond setting up this environment, Speakeasy has CLI commands that allow SDK users to effortlessly generate example usage snippets for any API operation within this container setting. SDK Sandboxes are currently available in Go, TypeScript, and Python SDKs. ## Configuring SDK Sandboxes To set up the automatically generated dev container configurations, make minor adjustments to the `gen.yaml` file. The `schemaPath` shown below should reference the SDK's OpenAPI document, which can be a local path or a remote URL. **Dev container `gen.yaml`:** ```yaml configVersion: 1.0.0 generation: devContainers: enabled: true schemaPath: ./openapi.yaml ``` Generation will create a `.devcontainer` directory containing a configuration file similar to below. **Generated `devcontainer.json`:** ```json // For format details, see https://aka.ms/devcontainer.json. For config options, see the // README at: https://github.com/devcontainers/templates/tree/main/src/go { "name": "Go", "image": "mcr.microsoft.com/devcontainers/go:1-1.20-bullseye", // Features to add to the dev container. More info: https://containers.dev/features. // "features": {}, // Use 'forwardPorts' to make a list of ports inside the container available locally. // "forwardPorts": [], // Use 'postCreateCommand' to run commands after the container is created. "postCreateCommand": "sudo chmod +x ./.devcontainer/setup.sh && ./.devcontainer/setup.sh", "customizations": { "vscode": { "extensions": [ "golang.go", "github.vscode-pull-request-github" // Github interaction ], "settings": { "files.eol": "\n", "editor.formatOnSave": true, "go.buildTags": "", "go.toolsEnvVars": { "CGO_ENABLED": "0" }, "go.useLanguageServer": true, "go.testEnvVars": { "CGO_ENABLED": "1" }, "go.testFlags": [ "-v", "-race" ], "go.testTimeout": "60s", "go.coverOnSingleTest": true, "go.coverOnSingleTestFile": true, "go.coverOnTestPackage": true, "go.lintTool": "golangci-lint", "go.lintOnSave": "package", "[go]": { "editor.codeActionsOnSave": { "source.organizeImports": true } }, "gopls": { "usePlaceholders": false, "staticcheck": true, "vulncheck": "Imports" } } }, "codespaces": { "openFiles": [ ".devcontainer/README.md" ] } } // Uncomment to connect as root instead. More info: https://aka.ms/dev-containers-non-root. // "remoteUser": "root" } ``` ## Using SDK Sandboxes in the browser SDK users can instantly initialize a sandbox in [GitHub Codespaces](https://docs.github.com/en/codespaces/overview) with a single click. By default, GitHub Codespaces shifts the cost to the user. However, organizations can be [configured](https://docs.github.com/en/codespaces/managing-codespaces-for-your-organization/enabling-or-disabling-github-codespaces-for-your-organization) to offer complimentary codespaces to users if preferred. ![Screenshot of a README featuring a dev container badge](/assets/docs/dev-container.png) # SDK versioning Source: https://speakeasy.com/docs/sdks/manage/versioning import { Screenshot } from "@/mdx/components"; Speakeasy-generated SDKs are automatically versioned using Semantic Versioning ([SemVer](https://semver.org/)). With each new generation, the SDK version is bumped up. ## Versioning logic The SDK version will be automatically incremented in the following cases. ### Generator version changes When Speakeasy releases a new generator version, it compares the features changed in the new generator to those used in the SDK: - If multiple used features in the SDK change, the largest version bump (major, minor, patch) across all used features determines the version increment. - Features unaffected by the new generator version maintain the current version. ### Configuration changes - Changes to the `gen.yaml` file will bump the patch version. - Changes to the checksum will also bump the patch version. ### OpenAPI document changes - If the `info.version` section of the OpenAPI document is SemVer compliant, major or minor changes to the OpenAPI document will bump the major or minor version of SDKs correspondingly. - _Coming Soon_: Speakeasy will detect changes to the OpenAPI document (for example, adding a breaking change to the parameters of an operation) and bump versions accordingly. ## Pre-release version bumps Speakeasy supports any SemVer-compatible string as a valid version, including pre-release versions such as `X.X.X-alpha` or `X.X.X-alpha.X`. - Pre-release versioning continues until manual removal. - Automated bumps increment pre-release versions. For example, `1.0.0-alpha`, `1.0.0-alpha.1`, `1.0.0-alpha.2`. - To exit pre-release versioning, set a new version or run `speakeasy bump graduate`. ## Major version bumps New SDKs start at version `0.0.1`. Automatic major version bumps begin after reaching version `1.0.0`. Breaking changes trigger major version increments after `1.0.0`. Major version changes affect the Golang SDK migration path. ### Golang major version bumps Golang module versions above `1.0.0` require import path changes to include the major version. For example: - Version `1.2.3`: `github.com/speakeasy/speakeasy-go` - Version `2.0.0`: `github.com/speakeasy/speakeasy-go/v2` Consider Golang SDK major version changes carefully due to migration path impacts. The SDK maintainer determines when to increment major versions. ## Disabling automatic versioning To permanently disable automatic version bumping, set `versioningStrategy` to `manual` in the `generation` section of your `gen.yaml` file: ```yaml generation: versioningStrategy: manual ``` When set to `manual`, the SDK version will only change when you explicitly update the `version` field in your language-specific configuration or use `speakeasy bump` commands. This is useful when you want full control over SDK versioning and don't want versions to change automatically based on spec, config, or generator changes. ## Manual version bumps Speakeasy supports manual control of SDK versioning through multiple methods. ### Via gen.yaml To override the automatic versioning logic for the next generation, set the `version` field in the `gen.yaml` file. - The Speakeasy generator detects manual version settings when the `releaseVersion` field in the `gen.lock` file differs from the `version` field in the `gen.yaml` file. - Automatic versioning will resume when the version values in both files match (unless `versioningStrategy` is set to `manual`). ### Via CLI commands There are two CLI commands you can use to manage SDK versions: 1. **Using `speakeasy run --set-version=...`**: This command allows you to specify a SemVer-compatible version when generating an SDK without modifying the gen.yaml file directly. This is a local change that will need to be pushed to GitHub to take effect. Examples: - `speakeasy run --set-version=1.2.3` - Sets the version to a standard release - `speakeasy run --set-version=1.2.3-rc.1` - Sets the version to a release candidate 2. **Using `speakeasy bump`**: Use the Speakeasy CLI `bump` command to set the SDK version without manually editing the `gen.yaml` file. This is a local change that will need to be pushed to GitHub to take effect. Examples: - `speakeasy bump patch` - Bumps the target's version by one patch version - `speakeasy bump -v 1.2.3` - Sets the target's version to 1.2.3 - `speakeasy bump -v 1.2.3-rc.1` - Sets the target's version to a release candidate - `speakeasy bump major -t typescript` - Bumps the typescript target's version by one major version - `speakeasy bump graduate` - Current version 1.2.3-alpha.1 sets the target's version to 1.2.3 ### Via GitHub Pull Request Labels Speakeasy supports label-based versioning via GitHub pull request (PR) UI: 1. Automated version detection: The system analyzes changes and suggests the appropriate semantic version bump. The generated PR displays a suggested version label: `major`, `minor`, or `patch`. A `pre-release` label is added for pre-release versions. 2. Manual override option: Override the suggestion by removing the current label and adding a `major`, `minor`, or `patch` label to the PR. Use the bump type `graduate` to move out of pre-release stage. The SDK version updates automatically on the next generation and persists across regenerations until changed. 3. Immediate generation: Label-based versioning is active in all SDK generation workflows. On newer versions of the CLI, the default sdk_generation.yaml already has the labels trigger, so this step may no longer be required. If your workflow file doesn't include this trigger, add the following action trigger to the GitHub workflow file (typically at `.github/workflows/sdk_generation.yaml`): ```yaml name: Generate permissions: checks: write contents: write pull-requests: write statuses: write "on": workflow_dispatch: inputs: force: description: Force generation of SDKs type: boolean default: false push_code_samples_only: description: Force push only code samples from SDK generation type: boolean default: false set_version: description: optionally set a specific SDK version type: string pull_request: types: [labeled] schedule: - cron: 0 0 * * * ``` ### Via GitHub Actions You can set the SDK version by manually running the GitHub workflow: 1. Go to the Actions tab in your GitHub repository 2. Select the SDK generation workflow 3. Click "Run workflow" 4. Use the "set_version" input to specify the desired version 5. Click "Run workflow" to start the process # Iterate on your OpenAPI document with the OpenAPI Editor Source: https://speakeasy.com/docs/sdks/prep-openapi The Speakeasy OpenAPI Editor provides a web-based interface for editing and managing OpenAPI specifications. The editor includes real-time validation, AI-powered suggestions, and a non-destructive overlay system that preserves source integrity while enabling customization. ## Edit with the overlay system The overlay system captures all changes as separate transformations that reapply automatically when the source specification regenerates. This approach prevents customizations from being overwritten. ![Screenshot of editor interface showing overlay indicator on modified fields](/assets/docs/prep-openapi/editor-overlay.png) ### How overlays work When editing a field: 1. Changes save as an overlay, not as direct modifications to the source 2. The overlay system tracks the specific path and transformation 3. On source regeneration, overlays reapply automatically 4. Conflicts surface in the editor for manual resolution This system is particularly useful for teams using code-first API development, where specifications generate from source code but require additional documentation or examples. For more information on OpenAPI Overlays, see [here](/docs/prep-openapi/overlays/create-overlays). ## Lint and validate specifications The editor provides real-time linting and validation to catch errors and improve specification quality. ![Screenshot of validation panel showing errors, warnings, and AI suggestions](/assets/docs/prep-openapi/editor-validation.png) ### Built-in validation Automatic validation catches common issues: - **Syntax errors**: Invalid YAML or JSON structure - **Schema violations**: Fields that don't conform to OpenAPI specification - **Reference errors**: Broken `$ref` pointers to components - **Semantic issues**: Logical inconsistencies like duplicate operation IDs Errors and warnings appear inline in the editor and in the validation panel. Select any issue to jump directly to the problematic line. For a full list of linting rules please see, [here](/docs/prep-openapi/linting). ## Preview Artifact changes The preview pane displays how specification changes affect generated SDKs and documentation in real-time. ![Screenshot of split-pane editor with source on left and SDK preview on right](/assets/docs/prep-openapi/editor-sdk-preview.png) ### Available preview modes Toggle between preview modes to explore the different types of artifacts you can generate: - **SDKs**: Generated client libraries in supported languages - **MCP servers**: Method signatures, parameters, and return types - **Terraform (Coming Soon!)**: Resource definitions and usage examples Changes made to the spec in the editor update the preview instantly, providing immediate feedback on how modifications affect the developer experience. ### Multi-language preview Select target languages to preview SDK generation across different programming environments: ![Screenshot of language selector showing TypeScript, Python, Go options](/assets/docs/prep-openapi/editor-language-selector.png) The preview uses the same generation logic as production SDK builds, ensuring accuracy. ## Next steps The OpenAPI Editor streamlines specification management through its overlay system, real-time validation, and instant SDK preview capabilities. Import specifications from any source, edit safely with non-destructive overlays, and validate changes before SDK generation. The editor integrates seamlessly into existing CLI workflows, requiring no changes to build pipelines or development processes. # OpenAPI overview and best practices Source: https://speakeasy.com/docs/sdks/prep-openapi/best-practices OpenAPI is a standard for describing RESTful APIs. OpenAPI defines an API's core elements, like endpoints, request and response data formats, authentication methods, and more. Several versions of the OpenAPI Specification are in circulation: 2.0 (known as Swagger), 3.0, and 3.1. Speakeasy supports OpenAPI versions 3.0 and 3.1 and recommends OpenAPI version 3.1 for all projects. The advantage of OpenAPI version 3.1 is its full compatibility with [JSON Schema](https://json-schema.org/), providing access to a large ecosystem of tools and libraries. ## OpenAPI best practices OpenAPI offers extensive flexibility and can describe any HTTP API, including REST APIs and even RPC-based calls. The OpenAPI Specification provides several valid approaches to achieving the same result. With this flexibility, constructing an OpenAPI document suitable for code generation requires careful consideration. Speakeasy recommends following specific best practices when writing OpenAPI documents. The sections below outline key points to consider when creating an OpenAPI description. ## servers Add multiple `servers` to define different environments or versions. This is especially useful for separating production and testing environments. ```yaml servers: - url: https://speakeasy.bar description: The production server - url: https://speakeasy.bar/testing description: The testing server ``` ## tags The `tags` property contains an optional list of values used to group or categorize a set of operations. **_Strongly recommended_** to always define `tags` for operations, though not required. **In code generation** Tags namespace methods in the SDK. For example, with a tag called `drinks`, all methods for that tag will be namespaced under `drinks.listDrinks()`. Create multi-level namespaces by using a `.` in the tag name, for example, `menu.drinks` becomes `menu.drinks.listDrinks()`. ```yaml tags: - drinks ``` ## operationId The `operationId` value is a unique identifier for the operation. It **_must_** be unique in the document and is **_case sensitive_**. **_Strongly recommended_** to always define an `operationId`, though not required. **In code generation** The `operationId` value creates the name of the method generated for the operation. Follow a consistent pattern for naming operations, for example, `listDrinks`, `createDrink`, `updateDrink`, and `deleteDrink`. When generating a spec from an API framework, ensure `operationId` values are human-readable. Some frameworks, like FastAPI, create long `operationId` identifiers that result in non-idiomatic method names. ```yaml operationId: listDrinks ``` ## examples Adding `examples` improves OpenAPI document usability by providing examples that illustrate the expected request and response structures. ```yaml examples: exampleResponse: summary: Example response for a list of drinks value: drinks: - name: "Coffee" price: 2.5 - name: "Tea" price: 1.8 ``` ## $ref In OpenAPI, the `$ref` keyword references components defined in the Components Object. These components are commonly used for reusable elements like schemas, parameters, responses, and examples. **In code generation** Component schemas describe the request and response bodies of operations, serving as the basis for generating SDK types. Using components prevents issues where multiple types are defined for the same data structure. ```yaml $ref: "#/components/schemas/Drink" components: schemas: Drink: type: object title: Drink properties: name: type: string price: type: number description: A response containing a list of drinks. BadRequest: type: object title: BadRequest properties: error: type: string message: type: string ``` **Dedicated Error Classes** Define dedicated error classes by creating response objects with specific HTTP status codes, such as `400 Bad Request` or `404 Not Found`, accompanied by clear descriptions and structured schemas to convey detailed error information. If the error class name doesn't clearly indicate the error type, use the `x-speakeasy-name-override` extension to rename it. ```yaml "400": description: Bad request content: application/json: schema: $ref: "#/components/schemas/BadRequest" ``` ## title The `title` property provides a human-readable title for each schema, improving the readability of the OpenAPI document. ```yaml title: Drink ``` ## description Use the `description` field to provide clear and concise information about the purpose, behavior, and expected usage of API elements. ```yaml description: A response containing a list of drinks. ``` ## Extending OpenAPI The OpenAPI Specification lacks an exhaustive vocabulary for describing API functionality. To overcome specification gaps, add extension fields to an OpenAPI document that describe additional metadata and functionality. Extensions typically follow a naming format of `x--`, where `` is the name of the vendor or tool that created the extension and `` is the goal accomplished by the extension. A [range of Speakeasy extensions](/docs/customize-sdks/) help prepare OpenAPI documents for code generation. Some of the most commonly used extensions are described below. ## x-speakeasy-name-override This extension overrides the name of a class, operation, or parameter. The most common use case is overriding `operationId` values in an OpenAPI document to simplify the generated SDK method names. When `operationId` identifiers follow a consistent pattern, define the name override globally using a regular expression to match the `operationId` and replace it with a new name. In this instance, the SDK will contain a method `menu.drinks.list()` rather than the longer `menu.drinks.list_drinks_v2_get()`. ```yaml x-speakeasy-name-override: - operationId: ^list_.* methodNameOverride: list operationId: list_drinks_v2_get ``` ## x-speakeasy-group Sometimes, the `tags` in an OpenAPI document may already serve an unrelated purpose, such as autogenerating labels in documentation. In this scenario, using something other than `tags` to organize and group methods may be necessary. Add the `x-speakeasy-group` field to any operation in the OpenAPI document to define custom namespaces and override any `tags` associated with the method. In this case, the `listDrinks` operation is added to a `menu.drinks` namespace rather than a `menu` namespace. ```yaml x-speakeasy-group: menu.drinks ``` ## x-speakeasy-usage-example Documentation forms an important part of any SDK. This extension allows selecting which operation is featured at the top of the `README.md`. Select the API operation that users frequently use. At a Speakeasy, that would likely be getting the list of drinks on offer. ```yaml x-speakeasy-usage-example: true ``` ## x-speakeasy-example Another useful documentation extension is `x-speakeasy-example`, which provides an example value to be used in the `Authentication` section of the SDK `README.md`. This example signals to users that the SDK should be instantiated with their security token. ```yaml x-speakeasy-example: "" ``` # OpenAPI document linting Source: https://speakeasy.com/docs/sdks/prep-openapi/linting import { Table } from "@/mdx/components"; In addition to running validation, Speakeasy lints OpenAPI documents to ensure stylistic validity. By default, the linter runs using a [recommended set of rules](/docs/linting/linting#speakeasy-recommended), which can be extended with the [available ruleset](/docs/linting/linting#available-rulesets). Custom rules can also be written using the Spectral format. The Speakeasy Linter offers: - Linting and validation of OpenAPI 3.x documents - Access to 70+ rules - Quick start with five out-the-box rulesets: `speakeasy-recommended`, `speakeasy-generation`, `speakeasy-openapi`, `vacuum`, and `owasp` - Reconfiguration of Speakeasy default rules and rulesets - Configuration of custom rulesets - Definition of new rules using the Spectral rule format - Support for custom functions written in Go and JavaScript ## Usage Three options are available for running linting: 1. Run manually via the Speakeasy CLI: ```bash speakeasy lint openapi -s openapi.yaml ``` 2. Integrate into a Speakeasy workflow: ```yaml workflowVersion: "1.0.0" speakeasyVersion: latest sources: api-source: inputs: - location: ./openapi.yaml ``` Running `speakeasy run` lints the document as part of the workflow and generates an HTML report accessible from a link in the command output. By default, these options use the `speakeasy-recommended` ruleset to ensure OpenAPI documents meet the Speakeasy quality bar. ## Configuration OpenAPI spec linting is fully configurable. Create custom rulesets by selecting from predefined sets or writing new rules. These custom linting rules work throughout the workflow. Immediately before SDK generation, the `speakeasy-generation` ruleset is always applied to ensure compatibility with the code generator. Configure linting in a `lint.yaml` document in the `.speakeasy` folder. The `.speakeasy` folder can be located in the same directory as the OpenAPI document, the working directory for running the `speakeasy lint` or `speakeasy run` commands, or the home directory. Example linting configuration in a `lint.yaml` file: ```yaml lintVersion: 1.0.0 defaultRuleset: speakeasyBarRuleset rulesets: barRuleset: rulesets: - speakeasy-generation # Use the speakeasy-generation ruleset as a base - customRuleset rules: validate-enums: { severity: warn, # drop the severity of the `validate-enums` rule to avoid blocking the pipeline } customRuleset: rules: paths-kebab-case: # A custom rule following the spectral format for a rule description: Paths should be kebab-case. message: "{{property}} should be kebab-case (lower-case and separated with hyphens)" severity: warn given: $.paths[*]~ then: functionOptions: match: "^(\\/|[a-z0-9-.]+|{[a-zA-Z0-9_]+})+$" contact-properties: description: Contact object must have "name", "url", and "email". given: $.info.contact severity: warn then: - field: name function: truthy - field: url function: truthy - field: email function: truthy ``` A `lint.yaml` document defines a collection of rulesets that can be chained together or used independently. Define any built-in Speakeasy rulesets, create new rules, modify existing rules, or remix available rules to suit specific needs. Use rulesets in these ways: 1. Set the `defaultRuleset` in the `lint.yaml` to use by default. This ruleset applies when no ruleset is specified using the `lint` command or `workflow.yaml` file. 2. Pass a ruleset name to the `lint` command with the `-r` argument, for example, `speakeasy lint openapi -r barRuleset -s openapi.yaml`. 3. Define the ruleset for a particular source in the `workflow.yaml` file. ```yaml workflowVersion: "1.0.0" speakeasyVersion: latest sources: api-source: inputs: - location: ./openapi.yaml ruleset: barRuleset ``` ## Custom rules Create custom rules most easily using the [Spectral rule format](https://docs.stoplight.io/docs/spectral/d3482ff0ccae9-rules), which defines the rule and its properties as an object. This format works for overriding built-in rules or defining new rules. For more complex rules beyond pattern matching or field existence checking, define custom Go or JavaScript functions. ### Custom Go/JavaScript functions Speakeasy linting builds on [vacuum](https://github.com/daveshanley/vacuum) and supports custom Go or JavaScript functions in rules. The vacuum documentation explains writing custom functions in [JavaScript](https://quobix.com/vacuum/api/custom-javascript-functions/) and [Go](https://quobix.com/vacuum/api/custom-functions/). After creating custom functions, place them in a directory named `functions` in the `.speakeasy` folder (e.g. `.speakeasy/functions/foo.js`). ## Available rules The rules available to the Speakeasy Linter are listed below and can be used in custom rulesets or to match and modify default rules in the `lint.yaml` file.
` tags.", }, { ruleId: "owasp-rate-limit-retry-after", defaultSeverity: "error", description: "Ensure that any `429` response contains a `Retry-After` header.", }, { ruleId: "oas3-valid-schema-example", defaultSeverity: "warn", description: "If an example has been used, check the schema is valid.", }, { ruleId: "typed-enum", defaultSeverity: "warn", description: "Enum values must respect the specified type.", }, { ruleId: "operation-operationId", defaultSeverity: "error", description: "Every operation must contain an `operationId`.", }, { ruleId: "operation-parameters", defaultSeverity: "error", description: "Operation parameters are unique and non-repeating.", }, { ruleId: "validate-requests", defaultSeverity: "error", description: "Validate request content types are valid MIME types.", }, { ruleId: "validate-document", defaultSeverity: "error", description: "Document must have a `paths` or `webhooks` object.", }, { ruleId: "no-ambiguous-paths", defaultSeverity: "error", description: "Paths need to resolve unambiguously from one another.", }, { ruleId: "oas3-api-servers", defaultSeverity: "warn", description: "Check for valid API servers definition.", }, { ruleId: "info-contact", defaultSeverity: "warn", description: "Info section is missing contact details.", }, { ruleId: "paths-kebab-case", defaultSeverity: "warn", description: "Path segments must only use kebab case (no underscores or uppercase).", }, { ruleId: "openapi-tags", defaultSeverity: "warn", description: "Top-level spec `tags` must not be empty and must be an array.", }, { ruleId: "owasp-array-limit", defaultSeverity: "error", description: "Array size should be limited to mitigate resource exhaustion attacks.", }, { ruleId: "oas-schema-check", defaultSeverity: "error", description: "All document schemas must have a valid type defined.", }, { ruleId: "license-url", defaultSeverity: "info", description: "License should contain a URL.", }, { ruleId: "oas2-schema", defaultSeverity: "error", description: "OpenAPI 2.0 specification is invalid.", }, { ruleId: "operation-success-response", defaultSeverity: "warn", description: "Operation must have at least one `2xx` or `3xx` response.", }, { ruleId: "owasp-no-api-keys-in-url", defaultSeverity: "error", description: "API key has been detected in a URL.", }, { ruleId: "info-description", defaultSeverity: "error", description: "Info section is missing a description.", }, { ruleId: "oas3-missing-example", defaultSeverity: "warn", description: "Ensure everything that can have an example contains one.", }, { ruleId: "oas3-host-trailing-slash", defaultSeverity: "warn", description: "Server URL should not contain a trailing slash.", }, { ruleId: "owasp-string-restricted", defaultSeverity: "error", description: "String must specify a `format`, RegEx `pattern`, `enum`, or `const`.", }, { ruleId: "owasp-no-additionalProperties", defaultSeverity: "warn", description: "By default, JSON Schema allows additional properties, which can potentially lead to mass assignment issues.", }, { ruleId: "contact-properties", defaultSeverity: "info", description: "Contact details are incomplete.", }, { ruleId: "validate-security", defaultSeverity: "error", description: "Validate security schemes are correct.", }, { ruleId: "operation-operationId-unique", defaultSeverity: "error", description: "Every operation must have a unique `operationId`.", }, { ruleId: "validate-types", defaultSeverity: "error", description: "Ensure data types are valid for generation.", }, { ruleId: "validate-consts-defaults", defaultSeverity: "warn", description: "Ensure `const` and `default` values match their type.", }, { ruleId: "operation-singular-tag", defaultSeverity: "warn", description: "Operation cannot have more than a single tag defined.", }, { ruleId: "oas2-api-schemes", defaultSeverity: "warn", description: "OpenAPI host `schemes` must be present and a non-empty array.", }, { ruleId: "validate-anyof", defaultSeverity: "warn", description: "`anyOf` should only contain types that are compatible with each other.", }, { ruleId: "duplicate-schemas", defaultSeverity: "hint", description: "Inline object schemas must be unique.", }, { ruleId: "missing-examples", defaultSeverity: "hint", description: "Examples should be provided where possible.", }, { ruleId: "no-eval-in-markdown", defaultSeverity: "error", description: "Markdown descriptions must not have `eval()` statements.", }, { ruleId: "oas2-host-trailing-slash", defaultSeverity: "warn", description: "Host URL should not contain a trailing slash.", }, { ruleId: "owasp-no-numeric-ids", defaultSeverity: "error", description: "Use random IDs that cannot be guessed. UUIDs are preferred.", }, { ruleId: "duplicate-schema-name", defaultSeverity: "error", description: "Schema names must be unique when converted to class names.", }, { ruleId: "validate-responses", defaultSeverity: "error", description: "Validate response content types are valid MIME types.", }, { ruleId: "path-not-include-query", defaultSeverity: "error", description: "Path must not include query string.", }, { ruleId: "path-declarations-must-exist", defaultSeverity: "error", description: "Path parameter declarations must not be empty, for example, `/api/{}` is invalid.", }, { ruleId: "owasp-constrained-additionalProperties", defaultSeverity: "warn", description: "By default, JSON Schema allows additional properties, which can potentially lead to mass assignment issues.", }, { ruleId: "operation-description", defaultSeverity: "warn", description: "Operation description checks.", }, { ruleId: "owasp-integer-format", defaultSeverity: "error", description: "Integers should be limited to mitigate resource exhaustion attacks.", }, { ruleId: "owasp-integer-limit", defaultSeverity: "error", description: "Integers should be limited with `min` or `max` values to mitigate resource exhaustion attacks.", }, { ruleId: "validate-deprecation", defaultSeverity: "error", description: "Ensure correct usage of `x-speakeasy-deprecation-replacement` and `x-speakeasy-deprecation-message` extensions.", }, { ruleId: "validate-json-schema", defaultSeverity: "error", description: "Validate OpenAPI document against JSON Schema.", }, { ruleId: "validate-content-type", defaultSeverity: "error", description: "Validate content type schemas.", }, { ruleId: "owasp-define-error-validation", defaultSeverity: "warn", description: "Missing error response for `400`, `422`, or `4XX`. Ensure all errors are documented.", }, { ruleId: "owasp-rate-limit", defaultSeverity: "error", description: "Define proper rate limiting to avoid attackers overloading the API.", }, { ruleId: "oas3-parameter-description", defaultSeverity: "warn", description: "Parameter description checks.", }, { ruleId: "oas3-host-not-example.com", defaultSeverity: "warn", description: "Server URL should not point at example.com.", }, { ruleId: "component-description", defaultSeverity: "warn", description: "Component description check.", }, { ruleId: "oas3-operation-security-defined", defaultSeverity: "error", description: "`security` values must match a scheme defined in `components.securitySchemes`.", }, { ruleId: "path-keys-no-trailing-slash", defaultSeverity: "warn", description: "Path must not end with a slash.", }, { ruleId: "duplicate-operation-name", defaultSeverity: "error", description: "Duplicate operation names can cause SDK method name collisions. An SDK method name combines two parts: `group` and `methodName`, forming `sdk.{group}.{methodName}()`. The `methodName` is derived from `operationId` but can be overridden with `x-speakeasy-name-override`; if neither is provided, it falls back to a name generated from the HTTP path and HTTP method. The optional `group` comes from `x-speakeasy-group`; if absent, `tags` may be used.", }, { ruleId: "info-license", defaultSeverity: "info", description: "Info section should contain a license.", }, { ruleId: "owasp-protection-global-safe", defaultSeverity: "info", description: "Check if the operation is protected at operation level. Otherwise, check the global `security` property.", }, { ruleId: "operation-operationId-valid-in-url", defaultSeverity: "error", description: "`operationId` must use URL-friendly characters.", }, { ruleId: "validate-servers", defaultSeverity: "error", description: "Validate servers, variables, and `x-speakeasy-server-id` extension.", }, { ruleId: "validate-extensions", defaultSeverity: "error", description: "Validate `x-speakeasy-globals` extension usage.", }, { ruleId: "oas2-unused-definition", defaultSeverity: "warn", description: "Check for unused definitions and bad references.", }, { ruleId: "oas2-parameter-description", defaultSeverity: "warn", description: "Parameter description checks.", }, ]} columns={[ { key: "ruleId", header: "Rule ID" }, { key: "defaultSeverity", header: "Default Severity" }, { key: "description", header: "Description" }, ]} /> ## Available rulesets The rulesets available to the Speakeasy Linter are listed below and can be chained in custom rules. These rulesets will be used by default when custom linting configuration is not provided. ### speakeasy-recommended The Speakeasy Linter uses the `speakeasy-recommended` ruleset by default when no custom ruleset is provided. This ruleset is recommended to ensure OpenAPI documents meet the Speakeasy quality bar.
### speakeasy-generation The `speakeasy-generation` ruleset is used when generating an SDK from an OpenAPI document. This set of rules _must_ pass to successfully generate an SDK from an OpenAPI document. This ruleset can't be overridden or reconfigured when using the generator. Use the `speakeasy-generation` ruleset as appropriate to configure the linter to ensure an OpenAPI document is ready for generation.
### speakeasy-openapi The `speakeasy-openapi` ruleset is a minimal set of rules recommended to ensure OpenAPI documents are generally valid and ready to be used by most of the OpenAPI ecosystem.
### vacuum The `vacuum` ruleset is provided by the [vacuum project](https://github.com/daveshanley/vacuum), which the Speakeasy Linter is built on top of. This set of rules is recommended to ensure OpenAPI documents meet the vacuum quality bar.
### owasp The `owasp` ruleset is recommended to ensure OpenAPI documents meet the [Open Worldwide Application Security Project (OWASP)](https://owasp.org/www-project-api-security/) quality bar.
# Apply an overlay Source: https://speakeasy.com/docs/sdks/prep-openapi/overlays/apply-overlays import { Screenshot } from "@/mdx/components"; Speakeasy provides two options for applying overlays: - Option One: Add the overlay directly to the Speakeasy workflow file, ensuring automatic application with every generation - Option Two: Output a new OpenAPI document with the overlay applied, creating an updated reference document ## Option One: Add an overlay to a Speakeasy workflow The Speakeasy workflow supports using an overlay file to modify a source OpenAPI document. For more information on sources, refer to the documentation on [core Speakeasy concepts](/docs/sdks/core-concepts#sources). ### 1. Install the Speakeasy CLI ```bash brew install speakeasy-api/tap/speakeasy ``` ### 2. Choose the source Run `speakeasy configure sources` and select or create a source to add an overlay to the Speakeasy workflow file. ### 3. Add an overlay ### 4. Provide the overlay file path /> The overlay will now apply to the OpenAPI document as part of the Speakeasy workflow. Execute the workflow by running `speakeasy run`. ## Option Two: Create a new OpenAPI document with an overlay Creating a new OpenAPI document provides the ideal option for generating a new reference document. ### 1. Install the Speakeasy CLI ```bash brew install speakeasy-api/tap/speakeasy ``` ### 2. Validate the overlay Validate the overlay before applying it to ensure adherence to the OpenAPI Overlay Specification using this command: ```bash speakeasy overlay validate -o overlays.yaml ``` ### 3. Apply the overlay Apply the overlay to the OpenAPI document with this command. Replace `input-openapi.yaml` with the path to the original OpenAPI document, and `overlays.yaml` with the path to the overlay file: ```bash speakeasy overlay apply -s input-openapi.yaml -o overlays.yaml > combined.yaml ``` This command merges changes from the overlay file with the original OpenAPI document and outputs the result to a new file named `combined.yaml`. ### 4. Review the merged results The `combined.yaml` file contains the original OpenAPI document updated with modifications from the overlay. Review this file to confirm changes are applied as expected. # Create overlays Source: https://speakeasy.com/docs/sdks/prep-openapi/overlays/create-overlays import { Callout } from "@/mdx/components"; ## What are overlays? The [Overlay Specification](https://github.com/OAI/Overlay-Specification/tree/3f398c6d38ddb5b4e514bc6a5a5ec487a3293834) defines a way to create overlay documents to be merged with an OpenAPI document to update it with additional information. Overlays are particularly useful in scenarios where the same OpenAPI document serves multiple purposes across different workflows or teams. Instead of making direct changes to the primary spec or managing multiple versions, overlays maintain customizations separately. These customizations apply to an OpenAPI document in a new file, ensuring API specifications remain flexible and adaptive without altering the core document. ## Common use cases Overlays enable various customizations to API specifications. Common scenarios include: - [Adding Speakeasy extensions](/guides/sdks/overlays/overlays#adding-speakeasy-extensions): Enhance OpenAPI documents with custom Speakeasy extensions for additional functionality or metadata - [Adding examples to API documentation](/guides/sdks/overlays/overlays#adding-examples-to-api-documentation): Provide clear, concrete examples to clarify API usage - [Hiding internal APIs from a public SDK](/guides/sdks/overlays/overlays#hiding-internal-apis-from-a-public-sdk): Exclude internal API endpoints from public-facing SDK documentation for security and clarity [View more examples here](/guides/sdks/overlays/overlays). ## Creating an overlay Create an overlay by writing a new YAML document that specifies the modifications to the OpenAPI document. Speakeasy supports both manual and automatic overlay creation. Generate the differences between two OpenAPI document versions with the `compare` command in the [Speakeasy CLI](/docs/speakeasy-reference/cli/overlay/compare). ```bash speakeasy overlay compare --before=./openapi_original.yaml --after=./openapi.yaml > overlay.yaml ``` The compare feature helps identify differences across OpenAPI documents. Precise adjustments may require manual refinement of the generated overlay file. Validate and evaluate JSONPath expressions [here](https://jsonpath.com/). ## Anatomy of an overlay ### `overlay` **Required** – Specifies the Overlay Specification version used by the document, currently limited to 1.0.0. ```yaml overlay: 1.0.0 ``` ### `info` - `title`: **Required** – Describes the overlay's purpose - `version`: **Required** – Identifies the document's version ```yaml overlay: 1.0.0 info: title: Overlay to fix the Speakeasy bar version: 0.0.1 ``` ### `actions` **Required** – An array of ordered actions for the target document, with at least one object per action. ```yaml overlay: 1.0.0 info: title: Overlay to fix the Speakeasy bar version: 0.0.1 actions: - target: "$.tags" description: Add a Snacks tag to the global tags list update: - name: Snacks description: All methods related to serving snacks - target: "$.paths['/dinner']" description: Remove all paths related to serving dinner remove: true ``` #### `target` **Required** – Specifies a JSONPath query expression to identify the target objects in the target document. #### `description` **Optional** – Brief explanation of the action being performed. Supports CommonMark syntax for rich text representation. #### `update` **Optional** – Defines the properties and values to merge with the objects identified by the target. This property is disregarded if the `remove` property is set to `true`. #### `remove` **Optional** – A boolean value indicating whether to remove the target object from its map or array. Defaults to `false` if not specified. ## Helpful references: - [Common overlay examples](/guides/sdks/overlays/overlays) - [Common JSONPath examples](/guides/sdks/overlays/json-path-expressions) For a visual approach to creating or editing overlays, visit{" "} overlay.speakeasy.com . # Transformations Source: https://speakeasy.com/docs/sdks/prep-openapi/transformations Transformations are predefined functions that modify an OpenAPI document's structure. Speakeasy currently supports the following transformations: - **Remove unused components:** Removes components not referenced by any operation - **Filter operations:** Filters operations down to a defined set of operation IDs - **Cleanup:** Reformats the document for better readability and compliance with various parsing tools - **Format:** Changes the order of keys at various levels in the document for improved human readability - **Convert Swagger to OpenAPI:** Converts a Swagger 2.0 document to an OpenAPI 3.0.x document ## Transformations vs. overlays Transformations and overlays both modify OpenAPI documents but differ in key ways: - **Transformations** operate dynamically and account for changes to the underlying document, staying "always up to date." For example, if an OpenAPI document updates and a previously unused component becomes used by a newly-added operation, the `removeUnused` transformation will no longer remove it. - **Overlays** offer more flexibility and can transform almost any document into any other document. However, overlays remain static and do not adapt with the underlying document. For example, when removing an unused component, an overlay continues to remove it even if the underlying document changes and the component becomes used. ## Using transformations Apply transformations to an OpenAPI document in two ways: 1. [Using the CLI](#using-the-cli): The easiest and most flexible approach 2. [As part of a Speakeasy workflow](#in-workflow-files): The most powerful approach, requiring more initial setup ### Using the CLI Apply a transformation to an OpenAPI document with the `speakeasy openapi transform` command. The interactive CLI guides through the process. Example commands: - `speakeasy openapi transform remove-unused -s openapi.yaml` - `speakeasy openapi transform filter-operations -s openapi.yaml --operations=getPets,createPet` - `speakeasy openapi transform cleanup -s openapi.yaml` - `speakeasy openapi transform format -s openapi.yaml` - `speakeasy openapi transform convert-swagger -s swagger.yaml` ### In workflow files The greatest utility comes from using transformations in the `workflow.yaml` file. This keeps the OpenAPI document always up to date with desired transformations, though it requires more initial setup. Note that the `convert-swagger` transformation cannot be applied as part of a workflow. ### Remove unused components Remove unused components from the OpenAPI document, preventing inclusion in the generated SDK. ```yaml workflowVersion: 1.0.0 speakeasyVersion: latest sources: api-source: inputs: - location: ./openapi.yaml transformations: - removeUnused: true - filterOperations: operations: getPets, createPet include: true - cleanup: true - format: true ``` ### Filter operations Keep or remove specified operations from the OpenAPI document. ```yaml workflowVersion: 1.0.0 speakeasyVersion: latest sources: api-source: inputs: - location: ./openapi.yaml transformations: - removeUnused: true - filterOperations: operations: getPets, createPet include: true # exclude: true - cleanup: true - format: true ``` ### Cleanup Clean up the OpenAPI document for improved readability and compliance with various parsing tools. ```yaml workflowVersion: 1.0.0 speakeasyVersion: latest sources: api-source: inputs: - location: ./openapi.yaml transformations: - removeUnused: true - filterOperations: operations: getPets, createPet include: true - cleanup: true - format: true ``` ### Format Reorder keys at various document levels for enhanced human readability. ```yaml workflowVersion: 1.0.0 speakeasyVersion: latest sources: api-source: inputs: - location: ./openapi.yaml transformations: - removeUnused: true - filterOperations: operations: getPets, createPet include: true - cleanup: true - format: true ``` # Publish SDKs and MCP Servers Source: https://speakeasy.com/docs/sdks/publish-sdk import { Table, Callout, Screenshot } from "@/mdx/components"; ## Prerequisites - The Speakeasy CLI - Speakeasy generated SDKs or MCP servers - Access tokens or credentials for target package managers ## Supported package managers
## Naming packages
For global package managers like PyPI and Maven, ensure the package name is unique. ## Publishing packages ### 1. Start publishing configuration ```bash speakeasy configure publishing ``` Select the existing SDK targets to configure publishing. If no SDK targets are available, run `speakeasy configure targets` first. ### 2. Verify configuration files Once configuration is complete, the following files will be generated or updated: - `.speakeasy/workflow.yaml` – Speakeasy workflow configuration. - `.github/workflows/sdk_generation.yaml` – GitHub Action to generate SDKs. - `.github/workflows/sdk_publish.yaml` – GitHub Action to publish SDKs. ### 3. Set up repository secrets 1. Navigate to GitHub repository **Settings > Secrets & Variables > Actions**. 2. Click **New repository secret**. 3. Add `SPEAKEASY_API_KEY` (if needed) and any other required tokens (e.g., `NPM_TOKEN`). ### 5. Push changes and verify Commit and push the updated workflow files. Once GitHub receives the generated SDK, manually kick off publishing for the first version. ### 6. Complete language-specific configuration Java and C# require additional setup after running `speakeasy configure publishing`. #### Java Maven: Sonatype Central Portal (recommended) 1. Create a [Sonatype Central Portal account](https://central.sonatype.org/register/central-portal/) (if needed). 2. Generate a [Sonatype username and password for authentication](https://central.sonatype.org/publish/generate-portal-token/). Save these for step 5. 3. Create a [Sonatype namespace](https://central.sonatype.org/register/central-portal/#choosing-a-namespace). 4. Create a GPG key to [sign the artifacts](https://central.sonatype.org/publish/requirements/gpg/). Save these for step 5. - Install GnuPG: `brew install gnupg` - Generate a key: `gpg --gen-key`. Note the key ID (e.g., `CA925CD6C9E8D064FF05B4728190C4130ABA0F98`) and short ID (e.g., `0ABA0F98`). - Send the key: `gpg --keyserver keys.openpgp.org --send-keys ` - Note: The following key servers can also be used: `keyserver.ubuntu.com`, `keys.openpgp.org`, or `pgp.mit.edu` - Export the secret key: `gpg --export-secret-keys --armor > secret_key.asc` - The file `secret_key.asc` will contain the GPG secret key. 5. Store the following secrets Github actions secrets: - `OSSRH_USERNAME` (the Sonatype username generated in Step 2) - `OSSRH_PASSWORD` (the corresponding password/token generated in Step 2) - `JAVA_GPG_SECRET_KEY` (the exported GPG key) - `JAVA_GPG_PASSPHRASE` (the passphrase for your GPG key) 6. In the java section of `gen.yaml`, provide the additional configuration required for publishing to Maven: ```yaml java: #ensure the `groupID` matches the OSSRH org groupID: com.example #ensure the `artificatID` matches the artifact name: artifactID: example-sdk githubURL: github.com/org/repo companyName: My Company companyURL: https://www.mycompany.com companyEmail: info@mycompany.com ``` #### C# NuGet 1. Create a [NuGet](https://www.nuget.org) account. 2. Create a NuGet API key: - Set the **Package Owner** field to the user or organization that will "own" the SDK artifact. - Ensure the API key has the relevant **Push** scope. If the package already exists, the API key may not need "Push new packages and package versions" permissions. - Populate the **Glob Pattern** and **Available Packages** fields in a way that allows publishing the SDK. Use the `packageName` specified in the `gen.yaml` file. 3. Store the `NUGET_API_KEY` in the GitHub Actions secrets. 4. In the C# section of `gen.yaml` add: ```yaml csharp: packageName: MyPackageName # Ensure this matches the desired NuGet package ID packageTags: sdk api client # Provide space-separated tags for searching on NuGet enableSourceLink: true # Enables publishing with Source Link for better debugging includeDebugSymbols: true # Includes .pdb files for publishing a symbol package (.snupkg) ``` 5. In the `info` section of the OpenAPI document, describe what the package is for in the `description` property. It will be set as the [Package description](https://learn.microsoft.com/en-us/nuget/create-packages/package-authoring-best-practices#description), visible when searching for the package on NuGet. ```yaml openapi: 3.1.0 info: description: This description will be visible when searching for the package on NuGet. ``` 6. In the `externalDocs` section of the OpenAPI document, provide the website's homepage in the `url` property. It will be set as the [Project URL](https://learn.microsoft.com/en-us/nuget/create-packages/package-authoring-best-practices#project-url), visible in the package's "About" section. ```yaml openapi: 3.1.0 externalDocs: url: https://homepage.com/docs description: Public Docs ``` 7. In the root of the repository: - Add a `LICENSE[.md|.txt]` file (see [Licensing](https://learn.microsoft.com/en-us/nuget/create-packages/package-authoring-best-practices#licensing) for more details). - Add a 128x128 dimension image file called `icon[.jpg|.png]`to display on the NuGet package page. - Review the `NUGET.md` file, which is similar to the main `README.md` but excludes the `SDK Installation` and `Available Operations` sections. For more details, see [Editing SDK Docs](/docs/sdk-docs/edit-readme). #### PHP 1. Create a [Packagist](https://packagist.org) account and manually create the Packagist package. 2. Generate a Main API Token: - Navigate to [Packagist Profile](https://packagist.org/profile/) and log in. - Record the "Main API Token" under account settings. 3. Update GitHub Action Secrets with Packagist credentials: - Navigate to the repository's Github Secret Settings. - Update the following secrets: - `PACKAGIST_TOKEN`: Set this to the Main API Token from step 2. - `PACKAGIST_USERNAME`: Set this to Packagist username. 4. Store the following secrets in the GitHub Actions secrets: - `PACKAGIST_TOKEN` - `PACKAGIST_USERNAME` 5. Confirm that the publishing job is properly set up with the updated credentials. **Note: Package name is read from composer.json `name` field.** #### TypeScript: Publishing private packages to npm For TypeScript SDKs, publish private packages to npm by configuring both the package name and publish settings in the `gen.yaml` file. **Requirements for private npm packages:** - Package name must be scoped (e.g., `@organization/package-name`) - npm access token with appropriate permissions - `publishConfig.access: restricted` in package.json **Configuration steps:** 1. Set a scoped package name in the `gen.yaml`: ```yaml typescript: packageName: '@example/openapi' ``` 2. Add the private publishing configuration: ```yaml typescript: packageName: '@example/openapi' additionalPackageJSON: publishConfig: access: restricted ``` This configuration generates a `package.json` that looks like: ```json { "name": "@example/openapi", "version": "0.0.3", "author": "Speakeasy", "publishConfig": { "access": "restricted" } } ``` 3. Ensure the `NPM_TOKEN` (configured in [step 3](#3-set-up-repository-secrets)) has the necessary permissions to publish to the organization's private packages. For more details on `additionalPackageJSON` configuration options, see the [TypeScript configuration reference](/docs/speakeasy-reference/generation/ts-config#additional-json-package). #### TypeScript: npm granular access tokens The npm registry requires granular access tokens for publishing packages. Classic tokens are deprecated; use granular tokens instead. **Generating a granular access token:** 1. Log in to your [npm account](https://www.npmjs.com/) 2. Navigate to **Access Tokens** in your account settings 3. Click **Generate New Token** and select **Granular Access Token** 4. Configure the token with the following settings: - **Token name**: Choose a descriptive name (e.g., `speakeasy-sdk-publishing`) - **Expiration**: Default is 7 days, but can be extended up to 1 year for automation workflows - **Packages and scopes**: Select **Read and write** permission for the packages you want to publish - For organization packages, ensure you have the appropriate organization permissions 5. Copy the generated token and store it securely as the `NPM_TOKEN` secret in your GitHub repository Granular tokens expire after the configured period (default 7 days). For CI/CD workflows, set a longer expiration period (up to 1 year) and establish a process to rotate tokens before they expire to avoid publishing failures. ## Migrating from a monorepo to a dedicated repository When moving a Speakeasy-generated SDK from within a monorepo to its own dedicated repository, follow these steps to ensure a smooth transition. ### Setting up the new repository 1. Create a new repository (can be public or private) 2. Copy the SDK configuration files to the root of the new repository: - `.speakeasy/gen.yaml` - `.speakeasy/workflow.yaml` - `overlay.yaml` (if using) 3. Update package configuration files to reflect the new repository location: - For TypeScript: Update `package.json` - For Python: Update `setup.py` or `pyproject.toml` - For Go: Update `go.mod` - For other languages: Update the respective package metadata files ### GitHub Actions configuration Copy the GitHub Actions workflows to the new repository under `.github/workflows/`. Important considerations: - Remove any `working_directory` settings that referenced the old monorepo structure - Ensure the following secrets are configured in the new repository: - `GITHUB_TOKEN` - Package manager tokens (e.g., `NPM_TOKEN`, `PYPI_TOKEN`) - `SPEAKEASY_API_KEY` ### Benefits of a dedicated repository - Cleaner build process without workspace dependencies - Easier configuration of linting and other tools - Simplified version control and release management - Direct GitHub issues integration for better user support - Independent versioning and release cycles ## Publishing in the Speakeasy dashboard The SDK publishing tab in the Speakeasy dashboard provides an overview of the publishing history and offers various utilities for setting up and maintaining SDK publishing. If package manager secrets were not set during the initial SDK repo setup, the publishing dashboard provides an interface to attach these secrets to the repository. For GitHub actions set up with `mode:pr`, the publishing dashboard highlights open PRs in the SDK repo that are pending release. This view displays the exact SDK version that will be published upon merging the PR. # SDK Contract Testing Source: https://speakeasy.com/docs/sdks/sdk-contract-testing import { Callout } from "@/mdx/components"; SDK contract testing features are are considered `beta` maturity. Breaking changes may occur and not all languages are yet supported. Currently SDK testing is supported for TypeScript, Python, Go, and Java. Speakeasy verifies functionality by generating and running contract tests for your SDKs. ## Design philosophy Just as with our [SDK design philosophy](/docs/languages/philosophy), Speakeasy strives to deliver a best-in-class development experience for contract testing. Speakeasy's contract tests are designed to be: - **Human readable:** Speakeasy-generated tests avoid convoluted abstractions and are easy for developers to read and debug. - **Batteries-included:** Tests can be generated for SDK operations (when enabled), with the option to use a real API testing environment. A generated mock API server works out of the box, avoiding complex test environment setups, such as data seeding. - **Rich in coverage:** Generated contract tests verify all possible data fields to ensure full coverage. If data examples are not available, realistic example values for the field are used based on name, type, and format information. - **Customizable:** Custom tests are supported beside generated tests. - **Minimal dependencies:** Speakeasy avoids adding unnecessary dependencies, prioritizing native language libraries and incorporating third-party libraries only when the customer benefits far outweigh the cost of the extra dependency. - **Easy integration:** Speakeasy testing easily integrates into existing API development and testing workflows. - **Open standards:** No vendor lock-in necessary. ## Features Speakeasy's test generation uses the [Arazzo Specification](/openapi/arazzo) to generate tests for APIs. Arazzo is a simple, human-readable, and extensible specification for defining API workflows. Speakeasy's Arazzo-powered test generation provides the following features: - Contract tests for operations in the OpenAPI document (when test generation is enabled), including: - Generating or modifying a `.speakeasy/tests.arazzo.yaml` file to include tests for operations. - Using examples available in the OpenAPI document or autogenerating examples based on the field name, type, and format of schemas. - Generating a mock server capable of responding to API requests, making the tests functional. - Custom tests and workflows for any use case, with rich testing capabilities, such as: - Testing multiple operations. - Testing different inputs. - Validating the correct response is returned. - Running tests against a real API or mock server. - Configuring setup and teardown routines for complex E2E tests. ## Prerequisites To enable testing features, the following are required: - An existing, successfully generating SDK. - The [Speakeasy CLI](/docs/speakeasy-reference/cli/getting-started) or a GitHub repository with Actions enabled. - [Docker](https://www.docker.com/) or an equivalent container runtime (if using the mock server for local testing). - An [Enterprise tier account](/pricing). - Enable the SDK contract add-on in `settings/billing` under your account. ## Next Steps - [Bootstrapping SDK contract tests](/docs/sdk-contract-testing/bootstrapping-test-generation) - [Customize SDK contract tests](/docs/sdk-contract-testing/customizing-sdk-tests) - [Set up testing in GitHub Actions](/docs/sdk-contract-testing/github-actions) # Bootstrapping SDK Tests Source: https://speakeasy.com/docs/sdks/sdk-contract-testing/bootstrapping-test-generation import { Table } from "@/mdx/components"; Automatically generate tests for SDKs. Speakeasy can boostrap tests for all operations including any new operations added in the future. These tests use any examples available in the OpenAPI document if available, or autogenerate examples based on the field name, type, and format of schemas. Multiple tests per operation can be configured using the named examples detailed for parameters, request bodies and responses. By default these tests will run against a mock server to validate the correctness of the SDK's serialization and deserialization. Tests are boostrapped into a `.speakeasy/tests.arazzo.yaml` file in the SDK repo. Once the test exists it can be customized from that `.speakeasy/tests.arazzo.yaml` without being overwritten. ## Prerequisites The following are requirements for generating tests: - [Testing feature prerequisites](/docs/sdk-testing#prerequisites) are met. ## Enabling Test Generation Navigate to the SDK repo and run the following command: ```bash speakeasy configure tests ``` This command will enable both `generateTests` and `generateNewTests` settings in your [`gen.yaml`](/docs/speakeasy-reference/generation/gen-yaml) configuration file. Test generation and mock API server generation will be enabled when the following exist in the `generation` section of the configuration. ```yaml configVersion: 2.0.0 generation: # ... other existing configuration ... tests: generateTests: true # Controls whether tests are generated during speakeasy run generateNewTests: true # Controls whether new tests are added for new operations ``` The `generateTests` setting controls whether test generation is enabled when running [`speakeasy run`](/docs/speakeasy-reference/cli/run). When set to `true`, tests defined in the `.speakeasy/tests.arazzo.yaml` document will be generated. When set to `false`, tests won't be generated. The `generateNewTests` setting controls whether new tests are automatically added to the `.speakeasy/tests.arazzo.yaml` document when new operations are found in the OpenAPI specification. When enabling for the first time this will generate tests for all operations in the OpenAPI document. Then going forward it will only generate tests for any operations not already found in the `.speakeasy/tests.arazzo.yaml` file. ## Disabling test generation To completely disable test generation, delete the `.speakeasy/tests.arazzo.yaml` file from your repository: ```bash rm .speakeasy/tests.arazzo.yaml ``` The existence of this file is what triggers test generation. Once removed, no tests will be generated regardless of your configuration. ### Disable test generation for specific operations After enabling test generation, to disable generation of tests for a specific operation, explicitly set `x-speakeasy-test: false`: ```yaml paths: /example1: get: # This operation, without being explicitly disabled, will generate testing. # ... operation configuration ... /example2: get: # This operation will not generate testing. # ... other operation configuration ... x-speakeasy-test: false ``` ### Generated Test Location Generated test files are written in language-specific locations, relative to the root of the SDK:
If the mock server is also generated, its output will be in a `mockserver` directory under these locations. ## Next Steps - [Running SDK tests](/docs/sdk-testing/running-tests) - [Customize SDK tests](/docs/sdk-testing/customizing-sdk-tests) - [Setup testing in GitHub Actions](/docs/sdk-testing/github-actions) # Custom end-to-end API contract tests with Arazzo Source: https://speakeasy.com/docs/sdks/sdk-contract-testing/custom-contract-tests import { Callout, CodeWithTabs } from "@/mdx/components"; We highly recommend fully setting up [SDK tests](/docs/sdk-contract-testing) in all your SDK repositories before exploring custom contract tests. You can use Speakeasy to create custom end-to-end contract tests that run against a real API. This document guides you through writing more complex tests using the Arazzo Specification, as well as through the key configuration features for these tests, which include: - Server URLs - Security credentials - Environment variable-provided values [Arazzo](/openapi/arazzo) is a simple, human-readable, and extensible specification for defining API workflows. Arazzo powers custom test generation, allowing you to define rich tests capable of: - Testing multiple operations - Testing different inputs - Validating that the correct response is returned - Running against a real API or mock server - Configuring setup and teardown routines for complex end-to-end (E2E) tests The Arazzo Specification allows you to define sequences of API operations and their dependencies for contract testing, enabling you to validate whether your API behaves correctly across multiple interconnected endpoints and complex workflows. When a `.speakeasy/tests.arazzo.yaml` file is found in your SDK repo, the Arazzo workflow is used to generate tests for each of the workflows defined in the file. ## Prerequisites Before generating tests, ensure that you meet the [testing feature prerequisites](/docs/sdk-contract-testing#prerequisites). ## Writing custom end-to-end tests The following is an example Arazzo document defining a simple E2E test for the lifecycle of a user resource in the example API: ```yaml arazzo: 1.0.0 info: title: Test Suite summary: E2E tests for the SDK and API. version: 0.0.1 sourceDescriptions: - name: The API url: https://example.com/openapi.yaml type: openapi workflows: - workflowId: user-lifecycle steps: - stepId: create operationId: createUser requestBody: contentType: application/json payload: { "email": "Trystan_Crooks@hotmail.com", "first_name": "Trystan", "last_name": "Crooks", "age": 32, "postal_code": 94110, "metadata": { "allergies": "none", "color": "red", "height": 182, "weight": 77, "is_smoking": true, }, } successCriteria: - condition: $statusCode == 200 - condition: $response.header.Content-Type == "application/json" - condition: $response.body#/email == Trystan_Crooks@hotmail.com - condition: $response.body#/postal_code == 94110 outputs: id: $response.body#/id - stepId: get operationId: getUser parameters: - name: id in: path value: $steps.create.outputs.id successCriteria: - condition: $statusCode == 200 - condition: $response.header.Content-Type == "application/json" - condition: $response.body#/email == Trystan_Crooks@hotmail.com - condition: $response.body#/first_name == Trystan - condition: $response.body#/last_name == Crooks - condition: $response.body#/age == 32 - condition: $response.body#/postal_code == 94110 outputs: user: $response.body age: $response.body#/age - stepId: update operationId: updateUser parameters: - name: id in: path value: $steps.create.outputs.id requestBody: contentType: application/json payload: $steps.get.outputs.user replacements: - target: /postal_code value: 94107 - target: /age value: $steps.get.outputs.age successCriteria: - condition: $statusCode == 200 - condition: $response.header.Content-Type == "application/json" - condition: $response.body#/email == Trystan_Crooks@hotmail.com - condition: $response.body#/first_name == Trystan - condition: $response.body#/last_name == Crooks - condition: $response.body#/age == 32 - condition: $response.body#/postal_code == 94107 outputs: email: $response.body#/email first_name: $response.body#/first_name last_name: $response.body#/last_name metadata: $response.body#/metadata - stepId: updateAgain operationId: updateUser parameters: - name: id in: path value: $steps.create.outputs.id requestBody: contentType: application/json payload: { "id": "$steps.create.outputs.id", "email": "$steps.update.email", "first_name": "$steps.update.first_name", "last_name": "$steps.update.last_name", "age": 33, "postal_code": 94110, "metadata": "$steps.update.metadata", } successCriteria: - condition: $statusCode == 200 - condition: $response.header.Content-Type == "application/json" - condition: $response.body#/email == Trystan_Crooks@hotmail.com - condition: $response.body#/first_name == Trystan - condition: $response.body#/last_name == Crooks - condition: $response.body#/age == 33 - condition: $response.body#/postal_code == 94110 - stepId: delete operationId: deleteUser parameters: - name: id in: path value: $steps.create.outputs.id successCriteria: - condition: $statusCode == 200 ``` This workflow defines four steps, each of which feeds into the next: 1. Create a user 2. Retrieve that user via its new ID 3. Update the user 4. Delete the user This is possible because the workflow defines outputs for certain steps that serve as inputs for the following steps. The workflow generates the test shown below: { const sdk = new SDK({ serverURL: process.env["TEST_SERVER_URL"] ?? "http://localhost:18080", httpClient: createTestHTTPClient("user-lifecycle"), }); const createResult = await sdk.createUser({ email: "Trystan_Crooks@hotmail.com", firstName: "Trystan", lastName: "Crooks", age: 32, postalCode: "94110", metadata: { allergies: "none", additionalProperties: { color: "red", height: "182", weight: "77", is_smoking: "true", }, }, }); expect(createResult.httpMeta.response.status).toBe(200); expect(createResult.user?.email).toEqual("Trystan_Crooks@hotmail.com"); expect(createResult.user?.postalCode).toBeDefined(); expect(createResult.user?.postalCode).toEqual("94110"); const getResult = await sdk.getUser(assertDefined(createResult.user?.id)); expect(getResult.httpMeta.response.status).toBe(200); expect(getResult.user?.email).toEqual("Trystan_Crooks@hotmail.com"); expect(getResult.user?.firstName).toBeDefined(); expect(getResult.user?.firstName).toEqual("Trystan"); expect(getResult.user?.lastName).toBeDefined(); expect(getResult.user?.lastName).toEqual("Crooks"); expect(getResult.user?.age).toBeDefined(); expect(getResult.user?.age).toEqual(32); expect(getResult.user?.postalCode).toBeDefined(); expect(getResult.user?.postalCode).toEqual("94110"); const user = assertDefined(getResult.user); user.postalCode = "94107"; user.age = getResult.user?.age; const updateResult = await sdk.updateUser( assertDefined(createResult.user?.id), assertDefined(getResult.user), ); expect(updateResult.httpMeta.response.status).toBe(200); expect(updateResult.user?.email).toEqual("Trystan_Crooks@hotmail.com"); expect(updateResult.user?.firstName).toBeDefined(); expect(updateResult.user?.firstName).toEqual("Trystan"); expect(updateResult.user?.lastName).toBeDefined(); expect(updateResult.user?.lastName).toEqual("Crooks"); expect(updateResult.user?.age).toBeDefined(); expect(updateResult.user?.age).toEqual(32); expect(updateResult.user?.postalCode).toBeDefined(); expect(updateResult.user?.postalCode).toEqual("94107"); const updateAgainResult = await sdk.updateUser( assertDefined(createResult.user?.id), { id: assertDefined(createResult.user?.id), email: assertDefined(updateResult.user?.email), firstName: updateResult.user?.firstName, lastName: updateResult.user?.lastName, age: 33, postalCode: "94110", metadata: updateResult.user?.metadata, }, ); expect(updateAgainResult.httpMeta.response.status).toBe(200); expect(updateAgainResult.user?.email).toEqual("Trystan_Crooks@hotmail.com"); expect(updateAgainResult.user?.firstName).toBeDefined(); expect(updateAgainResult.user?.firstName).toEqual("Trystan"); expect(updateAgainResult.user?.lastName).toBeDefined(); expect(updateAgainResult.user?.lastName).toEqual("Crooks"); expect(updateAgainResult.user?.age).toBeDefined(); expect(updateAgainResult.user?.age).toEqual(33); expect(updateAgainResult.user?.postalCode).toBeDefined(); expect(updateAgainResult.user?.postalCode).toEqual("94110"); const deleteResult = await sdk.deleteUser( assertDefined(createResult.user?.id), ); expect(deleteResult.httpMeta.response.status).toBe(200); });`, }, { label: "Python", language: "python", code: `# tests/test_sdk.py import io import openapi from openapi import SDK import os import pytest from tests.test_client import create_test_http_client def test_sdk_user_lifecycle(): with SDK( server_url=os.getenv("TEST_SERVER_URL", "http://localhost:18080"), client=create_test_http_client("user-lifecycle"), ) as sdk: assert sdk is not None create_res = sdk.create_user( request=openapi.BaseUser( email="Trystan_Crooks@hotmail.com", first_name="Trystan", last_name="Crooks", age=32, postal_code="94110", metadata=openapi.Metadata( allergies="none", **{ "color": "red", "height": "182", "weight": "77", "is_smoking": "true", }, ), ) ) assert create_res is not None assert create_res.email == "Trystan_Crooks@hotmail.com" assert create_res.postal_code is not None assert create_res.postal_code == "94110" get_res = sdk.get_user(id=create_res.id) assert get_res is not None assert get_res.email == "Trystan_Crooks@hotmail.com" assert get_res.first_name is not None assert get_res.first_name == "Trystan" assert get_res.last_name is not None assert get_res.last_name == "Crooks" assert get_res.age is not None assert get_res.age == 32 assert get_res.postal_code is not None assert get_res.postal_code == "94110" user = get_res user.postal_code = "94107" user.age = get_res.age update_res = sdk.update_user(id=create_res.id, user=user) assert update_res is not None assert update_res.email == "Trystan_Crooks@hotmail.com" assert update_res.first_name is not None assert update_res.first_name == "Trystan" assert update_res.last_name is not None assert update_res.last_name == "Crooks" assert update_res.age is not None assert update_res.age == 32 assert update_res.postal_code is not None assert update_res.postal_code == "94107" update_again_res = sdk.update_user( id=create_res.id, user=openapi.User( id=create_res.id, email=update_res.email, first_name=update_res.first_name, last_name=update_res.last_name, age=33, postal_code="94110", metadata=update_res.metadata, ), ) assert update_again_res is not None assert update_again_res.email == "Trystan_Crooks@hotmail.com" assert update_again_res.first_name is not None assert update_again_res.first_name == "Trystan" assert update_again_res.last_name is not None assert update_again_res.last_name == "Crooks" assert update_again_res.age is not None assert update_again_res.age == 33 assert update_again_res.postal_code is not None assert update_again_res.postal_code == "94110" sdk.delete_user(id=create_res.id)`, }, { label: "Go", language: "go", code: `// tests/sdk_test.go package sdk_test import ( "context" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "openapi" "openapi/internal/utils" "os" "testing" ) func TestSDK_UserLifecycle(t *testing.T) { ctx := context.Background() s := openapi.New( openapi.WithServerURL(utils.GetEnv("TEST_SERVER_URL", "http://localhost:18080")), openapi.WithClient(createTestHTTPClient("user-lifecycle")), ) createRes, err := s.CreateUser(ctx, openapi.BaseUser{ Email: "Trystan_Crooks@hotmail.com", FirstName: openapi.String("Trystan"), LastName: openapi.String("Crooks"), Age: openapi.Float64(32), PostalCode: openapi.String("94110"), Metadata: &openapi.Metadata{ Allergies: openapi.String("none"), AdditionalProperties: map[string]string{ "color": "red", "height": "182", "weight": "77", "is_smoking": "true", }, }, }) require.NoError(t, err) assert.Equal(t, 200, createRes.HTTPMeta.Response.StatusCode) assert.Equal(t, "Trystan_Crooks@hotmail.com", createRes.User.Email) assert.NotNil(t, createRes.User.PostalCode) assert.Equal(t, openapi.String("94110"), createRes.User.PostalCode) getRes, err := s.GetUser(ctx, createRes.User.ID) require.NoError(t, err) assert.Equal(t, 200, getRes.HTTPMeta.Response.StatusCode) assert.Equal(t, "Trystan_Crooks@hotmail.com", getRes.User.Email) assert.NotNil(t, getRes.User.FirstName) assert.Equal(t, openapi.String("Trystan"), getRes.User.FirstName) assert.NotNil(t, getRes.User.LastName) assert.Equal(t, openapi.String("Crooks"), getRes.User.LastName) assert.NotNil(t, getRes.User.Age) assert.Equal(t, openapi.Float64(32), getRes.User.Age) assert.NotNil(t, getRes.User.PostalCode) assert.Equal(t, openapi.String("94110"), getRes.User.PostalCode) user := *getRes.User user.PostalCode = openapi.String("94107") user.Age = getRes.User.Age updateRes, err := s.UpdateUser(ctx, createRes.User.ID, user) require.NoError(t, err) assert.Equal(t, 200, updateRes.HTTPMeta.Response.StatusCode) assert.Equal(t, "Trystan_Crooks@hotmail.com", updateRes.User.Email) assert.NotNil(t, updateRes.User.FirstName) assert.Equal(t, openapi.String("Trystan"), updateRes.User.FirstName) assert.NotNil(t, updateRes.User.LastName) assert.Equal(t, openapi.String("Crooks"), updateRes.User.LastName) assert.NotNil(t, updateRes.User.Age) assert.Equal(t, openapi.Float64(32), updateRes.User.Age) assert.NotNil(t, updateRes.User.PostalCode) assert.Equal(t, openapi.String("94107"), updateRes.User.PostalCode) updateAgainRes, err := s.UpdateUser(ctx, createRes.User.ID, openapi.User{ ID: createRes.User.ID, Email: updateRes.User.Email, FirstName: updateRes.User.FirstName, LastName: updateRes.User.LastName, Age: openapi.Float64(33), PostalCode: openapi.String("94110"), Metadata: updateRes.User.Metadata, }) require.NoError(t, err) assert.Equal(t, 200, updateAgainRes.HTTPMeta.Response.StatusCode) assert.Equal(t, "Trystan_Crooks@hotmail.com", updateAgainRes.User.Email) assert.NotNil(t, updateAgainRes.User.FirstName) assert.Equal(t, openapi.String("Trystan"), updateAgainRes.User.FirstName) assert.NotNil(t, updateAgainRes.User.LastName) assert.Equal(t, openapi.String("Crooks"), updateAgainRes.User.LastName) assert.NotNil(t, updateAgainRes.User.Age) assert.Equal(t, openapi.Float64(33), updateAgainRes.User.Age) assert.NotNil(t, updateAgainRes.User.PostalCode) assert.Equal(t, openapi.String("94110"), updateAgainRes.User.PostalCode) deleteRes, err := s.DeleteUser(ctx, createRes.User.ID) require.NoError(t, err) assert.Equal(t, 200, deleteRes.HTTPMeta.Response.StatusCode) }`, } ]} /> ## Input and outputs ### Inputs There are various ways to provide an input for a step, including: - Defining it in the workflow - Referencing it in a previous step - Including it as an inline value **Workflow inputs** You can provide input parameters to the workflow using the `inputs` field, which is a JSON Schema object that defines a property for each input that the workflow wants to expose. These [workflow inputs](/openapi/arazzo#workflow-object) can be used by any step defined in the workflow. Test generation can use any of the examples defined for a property in an `inputs` JSON schema as a literal value, which it then uses as an input for the test. Because tests are non-interactive and cannot ask users for input, the test generation randomly generates values for the inputs if no examples are defined. ```yaml arazzo: 1.0.0 # .... workflows: - workflowId: some-test inputs: # This is the JSON Schema for the inputs each property in the inputs object represents a workflow input type: object properties: email: type: string examples: - Trystan_Crooks@hotmail.com # Examples defined will be used as literal values for the test firstName: type: string examples: - Trystan lastName: type: string examples: - Crooks steps: - stepId: create operationId: createUser requestBody: contentType: application/json payload: { "email": "$inputs.email", # The payload will be populated with the literal value defined in the inputs "first_name": "$inputs.firstName", "last_name": "$inputs.lastName", } successCriteria: - condition: $statusCode == 200 ``` **Step references** Parameters and request body payloads can reference values via [runtime expressions](/openapi/arazzo#runtime-expressions) from previous steps in the workflow. This allows for the generation of tests that are more complex than a simple sequence of operations. Speakeasy's implementation currently only allows the referencing of a previous step's output, which means you will need to define which values you want to expose to future steps. ```yaml arazzo: 1.0.0 # .... workflows: - workflowId: some-test steps: - stepId: create operationId: createUser requestBody: contentType: application/json payload: #.... successCriteria: - condition: $statusCode == 200 - condition: $response.header.Content-Type == "application/json" - condition: $response.body#/email == Trystan_Crooks@hotmail.com outputs: id: $response.body#/id # The id field of the response body will be exposed as an output for the next step - stepId: get operationId: getUser parameters: - name: id in: path value: $steps.create.outputs.id # The id output from the previous step will be used as the value for the id parameter successCriteria: - condition: $statusCode == 200 ``` **Inline values** For any parameters or request body payloads that a step defines, you can also provide literal values inline to populate the tests (if static values are suitable for the tests). ```yaml arazzo: 1.0.0 # .... workflows: - workflowId: some-test steps: - stepId: update operationId: updateUser parameters: - name: id in: path value: "some-test-id" # A literal value can be provided inline for parameters that matches the json schema of the parameter as defined in the associated operation requestBody: contentType: application/json payload: # literals values that match the content type of the request body can be provided inline { "email": "Trystan_Crooks@hotmail.com", "first_name": "Trystan", "last_name": "Crooks", "age": 32, "postal_code": 94110, "metadata": { "allergies": "none", "color": "red", "height": 182, "weight": 77, "is_smoking": true, }, } successCriteria: - condition: $statusCode == 200 ``` **Payload values** If you use the `payload` field of a request body input, its value can be any of the following: - A static value - A value with interpolated [runtime expressions](/openapi/arazzo#runtime-expressions) - A [runtime expression](/openapi/arazzo#runtime-expression) by itself You can then overlay the payload value using the `replacements` field, which represents a list of targets within the payload that will be replaced with the value of the replacements. These replacements can be static values or [runtime expressions](/openapi/arazzo#runtime-expression). ```yaml arazzo: 1.0.0 # .... workflows: - workflowId: some-test steps: - stepId: get # ... outputs: user: $response.body - stepId: update operationId: updateUser parameters: - name: id in: path value: "some-test-id" requestBody: contentType: application/json payload: $steps.get.outputs.user # use the response body of the previous step as the payload for this step replacements: # overlay the payload with the below replacements - target: /postal_code # overlays the postal_code field with a static value value: 94107 - target: /age # overlays the age field with the value of the age output of a previous step value: $steps.some-other-step.outputs.age successCriteria: - condition: $statusCode == 200 ``` ### Outputs As shown above, you can define outputs for each of the steps in a workflow, allowing you to use values from things such as response bodies in following steps. Currently, Speakeasy supports only referencing values from a response body, using the [runtime expressions](/openapi/arazzo#runtime-expressions) syntax and json-pointers. Any number of outputs can be defined for a step. ```yaml arazzo: 1.0.0 # .... workflows: - workflowId: some-test steps: - stepId: create operationId: createUser requestBody: contentType: application/json payload: #.... successCriteria: - condition: $statusCode == 200 - condition: $response.header.Content-Type == "application/json" - condition: $response.body#/email == Trystan_Crooks@hotmail.com outputs: # Outputs are a map of an output id to a runtime expression that will be used to populate the output id: $response.body#/id # json-pointers are used to reference fields within the response body email: $response.body#/email age: $response.body#/age allergies: $response.body#/metadata/allergies ``` ## Success criteria A step's `successCriteria` field contains a list of [criterion objects](/openapi/arazzo#criterion-object) used to validate the success of the step and form the basis of the test assertions for test generation. The `successCriteria` may be as simple as a single condition testing the status code of the response or as complex as multiple conditions testing various individual fields within the response body. Speakeasy's implementation currently only supports `simple` criteria and the use of the equality (`==`) and inequality (`!=`) operators for comparing values and for testing status codes, response headers, and response bodies. **Note:** While the [Arazzo specification](/openapi/arazzo#operators) defines additional operators like `>`, `<`, `>=`, `<=`, `~`, and `!~`, Speakeasy currently only supports `==` and `!=`. To test values within the response body, due to the typed nature of the SDKs, you need criteria for testing the status code and content type of the response to help the generator determine which response schema to validate against. ```yaml arazzo: 1.0.0 # .... workflows: - workflowId: some-test steps: - stepId: create operationId: createUser requestBody: contentType: application/json payload: #.... successCriteria: - condition: $statusCode == 200 - condition: $response.header.Content-Type == "application/json" - condition: $response.body#/email == Trystan_Crooks@hotmail.com # or - context: $response.body type: simple condition: | { "email": "Trystan_Crooks@hotmail.com", "first_name": "Trystan", "last_name": "Crooks", "age": 32, "postal_code": 94110, "metadata": { "allergies": "none", "color": "red", "height": 182, "weight": 77, "is_smoking": true } } ``` ## Testing operations requiring binary data Some operations require you to provide binary data, for example, for testing file uploads and downloads. To provide the test with the test files, use the `x-file` directive in the example for the relevant field. ```yaml arazzo: 1.0.0 # .... workflows: - workflowId: postFile steps: - stepId: test operationId: postFile requestBody: contentType: multipart/form-data payload: file: "x-file: some-test-file.txt" successCriteria: - condition: $statusCode == 200 - condition: $response.header.Content-Type == "application/octet-stream" - context: $response.body condition: "x-file: some-other-test-file.dat" type: simple ``` The files are sourced from the `.speakeasy/testfiles` directory in the root of your SDK repo, where the path provided in the `x-file` directive is relative to the `testfiles` directory. The content of the sourced file is used as the value for the field being tested. ## Configuring an API to test against By default, tests are generated to run against Speakeasy's mock server, which has a URL of `http://localhost:18080`. The mock server validates that the SDKs are functioning correctly but does not guarantee that the API is correct. The generator can be configured to run all tests against another URL or against individual tests using the `x-speakeasy-test-server` extensions in the `.speakeasy/tests.arazzo.yaml` file. If the extension is found at the top level of the Arazzo file, all workflows and tests will be configured to run against the specified server URL. If the extension is found within a workflow, only that workflow will be configured to run against the specified server URL. The server URL can be either a static URL or an `x-env: EXAMPLE_ENV_VAR` value that pulls the value from the environment variable, `EXAMPLE_ENV_VAR` (where the name of the environment variable can be any specified name). ```yaml arazzo: 1.0.0 # ... x-speakeasy-test-server: baseUrl: "https://api.example.com" # If specified at the top level of the Arazzo file, all workflows will be configured to run against the specified server URL workflows: - workflowId: some-test x-speakeasy-test-server: baseUrl: "x-env: CUSTOM_API_URL" # If specified within a workflow, only that workflow will be configured to run against the specified server URL. This will override any top-level configuration. # ... ``` You may provide a default value in the `x-env` directive if the environment variable is not set. This can be useful for local development or non-production environments. ```yaml x-speakeasy-test-server: baseUrl: "x-env: CUSTOM_API_URL; http://localhost:18080" # Run against the local mock server if the environment variable is not set ``` The `TEST_SERVER_URL` environment variable is reserved for use by Speakeasy's mock server. When running tests via `speakeasy test`, if the mock server is generated and enabled, `TEST_SERVER_URL` is automatically set to the URL of the running mock server and overwrites any existing value for that environment variable while running. If you want to use a custom test server instead of the mock server, you can: - Use the `--disable-mockserver` flag when running `speakeasy test` to prevent the automatic setting of `TEST_SERVER_URL` - Use a different environment variable name (like `CUSTOM_API_URL` in the examples above) for your custom server configuration If all tests are configured to run against other server URLs, you can disable mock server generation in the `.speakeasy/gen.yaml` file: ```yaml # ... generation: # ... mockServer: disabled: true # Setting this to true will disable mock server generation ``` ## Configuring security credentials for contract tests When running tests against a real API, the SDK may need to be configured with security credentials to authenticate with the API. To configure the SDK, add the `x-speakeasy-test-security` extension to the document, workflow, or individual step. The `x-speakeasy-test-security` extension allows static values, values pulled from the environment, or runtime expressions referencing outputs from previous steps to be used when instantiating an SDK instance and making requests to the API. **Important:** The keys under `value` must exactly match the names of the `securitySchemes` defined in your OpenAPI document's `components.securitySchemes` section. For example, if your OpenAPI document defines: ```yaml components: securitySchemes: myApiKeyScheme: type: apiKey in: header name: X-API-Key myBasicAuthScheme: type: http scheme: basic myBearerTokenScheme: type: http scheme: bearer myOAuth2Scheme: type: oauth2 flows: clientCredentials: tokenUrl: https://api.example.com/oauth2/token scopes: {} ``` Then your Arazzo test configuration should reference these exact scheme names: ```yaml arazzo: 1.0.0 # ... x-speakeasy-test-security: # Defined at the top level of the Arazzo file, all workflows will be configured to use the specified security credentials value: # The keys below MUST match the securitySchemes names from your OpenAPI document myApiKeyScheme: "x-env: TEST_API_KEY" # Values can be pulled from the environment myBasicAuthScheme: username: "test-user" # For schemes requiring multiple values, provide a map password: "x-env: TEST_PASSWORD" myBearerTokenScheme: "x-env: TEST_BEARER_TOKEN" myOAuth2Scheme: clientId: "x-env: MY_CLIENT_ID" clientSecret: "x-env: MY_CLIENT_SECRET" tokenURL: "http://test-server/oauth2/token" # Redirect OAuth flow to test server workflows: - workflowId: some-test x-speakeasy-test-security: # Security can be defined/overridden for a specific workflow value: myApiKeyScheme: "test-key" # ... steps: - stepId: step1 x-speakeasy-test-security: # Or security can be defined/overridden for a specific step value: myBearerTokenScheme: "x-env: TEST_AUTH_TOKEN" # ... - stepId: step2 # ... ``` **Note:** For OAuth2 schemes, you can override the `tokenURL` to redirect the authentication flow to a test server instead of the production endpoint. This allows you to test OAuth2 flows against mock servers or staging environments without affecting production authentication systems. ### Using Runtime Expressions for Dynamic Security The `x-speakeasy-test-security` extension also supports runtime expressions, allowing you to populate security credentials dynamically from the outputs of previous steps. This is particularly useful for workflows that require authentication tokens obtained from login operations. For example, you can use a runtime expression to reference a token from a previous authentication step: ```yaml arazzo: 1.0.0 info: title: Example summary: Example of a test suite version: 0.0.1 sourceDescriptions: - name: ./example.yaml url: https://example.com type: openapi workflows: - workflowId: createUser x-speakeasy-test-security: value: apiKey: $steps.authenticate.outputs.token steps: - stepId: authenticate workflowId: authenticate requestBody: contentType: application/json payload: { "username": "trystan.crooks@example.com", "password": "x-env: TEST_PASSWORD", } successCriteria: - condition: $steps.authenticate.outputs.token != "" outputs: token: $steps.authenticate.outputs.token - stepId: create operationId: createUser requestBody: contentType: application/json payload: { "email": "Trystan_Crooks@hotmail.com", "first_name": "Trystan", "last_name": "Crooks", "age": 32, "postal_code": 94110, "metadata": { "allergies": "none", "color": "red", "height": 182, "weight": 77, "is_smoking": true, }, } successCriteria: - condition: $statusCode == 200 - condition: $response.header.Content-Type == "application/json" - condition: $response.body#/email == Trystan_Crooks@hotmail.com - condition: $response.body#/postal_code == 94110 outputs: id: $response.body#/id - stepId: get operationId: getUser parameters: - name: id in: path value: $steps.create.outputs.id successCriteria: - condition: $statusCode == 200 - condition: $response.header.Content-Type == "application/json" - condition: $response.body#/email == Trystan_Crooks@hotmail.com - condition: $response.body#/first_name == Trystan - condition: $response.body#/last_name == Crooks - condition: $response.body#/age == 32 - condition: $response.body#/postal_code == 94110 ``` In this example: - The workflow defines `x-speakeasy-test-security` at the workflow level with `apiKey: $steps.authenticate.outputs.token` - The first step (`authenticate`) calls an authentication workflow and captures the token in its outputs - All subsequent steps in the workflow will use this dynamically obtained token for authentication - The runtime expression `$steps.authenticate.outputs.token` references the token output from the authenticate step This approach enables complex authentication flows where tokens must be obtained dynamically during test execution, rather than being provided as static values or environment variables. ## Configuring environment variable provided values for Contract tests When running tests against a real API, you may need to fill in certain input values from dynamic environment variables. Use the Speakeasy environment variable extension to do so: ```yaml arazzo: 1.0.0 # .... workflows: - workflowId: my-env-var-test steps: - stepId: update operationId: updateUser parameters: - name: id in: path value: "x-env: TEST_ID; default" # Provide an environment variable and an optional default value if that env variable is not present. requestBody: contentType: application/json payload: { "email": "x-env: TEST_EMAIL; default", # Provide an environment variable and an optional default value if that env variable is not present. "first_name": "Trystan", "last_name": "Crooks", "age": 32, "postal_code": 94110, "metadata": { "allergies": "none", "color": "red", "height": 182, "weight": 77, "is_smoking": true, }, } successCriteria: - condition: $statusCode == 200 ``` # Customizing SDK Tests Source: https://speakeasy.com/docs/sdks/sdk-contract-testing/customizing-sdk-tests import { Callout } from "@/mdx/components"; ## Test Generation Overview When `generateNewTests` is enabled in the [`.speakeasy/gen.yaml`](/docs/speakeasy-reference/generation/gen-yaml) file, workflows for any newly discovered operations will automatically be created in the `.speakeasy/tests.arazzo.yaml` file during the next generation. Tests will then be generated based on these workflows. Note that for tests to actually be generated during [`speakeasy run`](/docs/speakeasy-reference/cli/run), the `generateTests` setting must also be enabled. Both settings can be enabled by running [`speakeasy configure tests`](/docs/sdk-testing/bootstrapping-test-generation#enabling-test-generation). ## Disable auto generation of tests for specific operations To disable auto generation of tests for specific operations, the `x-speakeasy-test` extension can be added to the operation in the OpenAPI document: ```yaml openapi: 3.1.0 # ... paths: /example1: get: x-speakeasy-test: false # Disables auto generation of tests for this operation # ... # ... ``` Alternatively, if a workflow/test already exists that references the operation in the `.speakeasy/tests.arazzo.yaml` file, then no new workflow will be created for the operation. ## Automatic Test Synchronization Speakeasy synchronizes workflows in the `.speakeasy/tests.arazzo.yaml` file based on the presence of the `x-speakeasy-test-rebuild` extension. When `generateNewTests` is enabled, Speakeasy automatically applies this extension to newly created workflows. The extension indicates that the workflows should remain synchronized with any subsequent changes to the OpenAPI specification. ### Auto-sync with OpenAPI Changes (`x-speakeasy-test-rebuild: true`) When set to `true`, the workflow will automatically be kept in sync with the state of the operation in the OpenAPI specification. The test will be rebuilt each time the spec changes, incorporating: - New parameters - Request body field changes - Response body updates - Updates to examples This is useful if you want Speakeasy to automatically maintain your tests as your API evolves. ```yaml arazzo: 1.0.0 # ... workflows: - workflowId: some-test x-speakeasy-test-rebuild: true # Test will auto-sync with OpenAPI changes # ... ``` ### Manual Test Maintenance (`x-speakeasy-test-rebuild: false` or omitted) When the extension is omitted or set to `false`, the workflow needs to be maintained manually and won't sync with the OpenAPI specification. This gives you complete control over the test content and is useful when: - Writing custom tests with specific logic - Maintaining test state independent from the OpenAPI document - Catching breaking changes or regressions in the API behavior ```yaml arazzo: 1.0.0 # ... workflows: - workflowId: some-test x-speakeasy-test-rebuild: false # Test requires manual maintenance # ... ``` ## Grouping tests By default, all tests will be generated into a single file related to the main SDK class for example `sdk.test.ts` , `test_sdk.py`, or `SDKTest.java`. This can be configured by adding a `x-speakeasy-test-group` extension to the workflow which will determine which tests will be grouped together in seperate test files. ```yaml arazzo: 1.0.0 # ... workflows: - workflowId: some-test x-speakeasy-test-group: user # All tests in the user group will end up in the `user.test.ts`/`test_user.py`/`user_test.go` files # ... ``` ## Generate tests only for specific targets By default, all tests will be generated for all supported targets. Using the `x-speakeasy-test-targets` or `x-speakeasy-test-targets-exclude` extensions, it is possible to generate tests only for specific targets. This may be useful if tests are either not needed for certain languages or they may be failing in a certain language. ```yaml arazzo: 1.0.0 # ... workflows: - workflowId: some-test x-speakeasy-test-targets: # Only generate tests for the typescript target - typescript # ... ``` or ```yaml arazzo: 1.0.0 # ... workflows: - workflowId: some-test x-speakeasy-test-targets-exclude: # generate tests for all languages except typescript - typescript # ... ``` ## Customizing SDK Test Manifests Directly modifying SDK tests is not common, but it is possible to customize the actual content of an existing SDK test by modifying the `.speakeasy/tests.arazzo.yaml` file. To learn more about these kinds of modifications see [here](/docs/sdk-testing/api-contract-tests#writing-custom-end-to-end-tests). ## Next Steps - [Setup testing in GitHub Actions](/docs/sdk-testing/github-actions) Once the original `tests.arazzo.yaml` document is bootstrapped, the Arazzo document will only be modified automatically when new operations are detected and `generateNewTests: true` is set in your `./speakeasy/gen.yaml`, new tests will be bootstrapped for these new operations. But pre-bootstrapped tests will be left alone. If you want to bootstrap all workflows in the Arazzo document from scratch again (for example to match changes to operations or examples in your spec), run the following command: ```bash speakeasy configure tests --rebuild ``` You can also selectively bootstrap individual tests by: 1. Deleting the specific workflow from the arazzo doc in `.speakeasy/tests.arazzo.yaml` 2. Removing the corresponding entry in `.speakeasy/gen.lock` under `generatedTests` For example, to bootstrap a test for the `createLink` operation again, delete its entry in the gen.lock file: ```yaml generatedTests: createLink: "2025-05-21T12:47:33+10:00" # Delete this to bootstrap test again from scratch ``` This approach preserves any other manual changes you've made to your arazzo doc. # Testing in GitHub Actions Source: https://speakeasy.com/docs/sdks/sdk-contract-testing/github-actions import { Callout } from "@/mdx/components"; import { Screenshot } from "@/mdx/components"; Automatically run Speakeasy tests on SDK pull requests or other events in a GitHub repository via GitHub Actions. ## Setting up a Github Actions Check This requires previously completing the [Github Setup](/docs/manage/github-setup). After completing the setup, navigate to the SDK repository and run the following command: ```bash # It's okay to run this command multiple times if you have already configured tests locally speakeasy configure tests ``` This command enables both `generateTests` and `generateNewTests` settings in your [`gen.yaml`](/docs/speakeasy-reference/generation/gen-yaml) configuration file and produces a Github Actions workflow like the following that enables running SDK tests as a Github check on Pull Requests. ```yaml name: Test SDKs permissions: checks: write contents: write pull-requests: write statuses: write on: workflow_dispatch: inputs: target: description: Specific target to test type: string pull_request: paths: - "**" branches: - main jobs: test: uses: speakeasy-api/sdk-generation-action/.github/workflows/sdk-test.yaml@v15 with: target: ${{ github.event.inputs.target }} secrets: github_access_token: ${{ secrets.GITHUB_TOKEN }} speakeasy_api_key: ${{ secrets.SPEAKEASY_API_KEY }} ``` ## Ensuring Tests Run on Automated PR Creation Pull requests created by the action using the default GITHUB_TOKEN cannot trigger other workflows. When you have on: pull_request or on: push workflows acting as checks on pull requests, they will not run by default. To ensure that testing checks run by default when an SDK PR is created, implement one of the following options. ### Installing the Speakeasy Github App Installing the Speakeasy Github App and granting the App access to the SDK repository enables triggering testing runs after a PR is created. To install the app, visit this [link](https://github.com/apps/speakeasy-github) or follow instructions in the CLI or dashboard. ### Setting up your own Github PAT Another option is to create a Github Personal Access Token (PAT) that will be used to create PRs in the SDK repository. This is a workaround [recommended](https://docs.github.com/en/actions/writing-workflows/choosing-when-your-workflow-runs/triggering-a-workflow#triggering-a-workflow-from-a-workflow) by Github. 1. Create a [fine-grained](https://docs.github.com/en/authentication/keeping-your-account-and-data-secure/managing-your-personal-access-tokens#creating-a-fine-grained-personal-access-token) PAT with `Pull requests Read/Write` permissions. Ensure it has access to the SDK repository. Setting this to no expiration is recommended. 2. In all SDK repositories, navigate to `Settings > Secrets and variables > Actions` and save the PAT as a Repository secret under the name `PR_CREATION_PAT`. 3. In all sdk_generation.yaml workflows, add the `pr-creation-token` as a provided secret at the bottom. ```yaml secrets: github_access_token: ${{ secrets.GITHUB_TOKEN }} speakeasy_api_key: ${{ secrets.SPEAKEASY_API_KEY }} pr_creation_pat: ${{ secrets.PR_CREATION_PAT }} ``` ## Running in Direct mode If your generation action is running in `direct` mode where SDK updates get immediately pushed to main, testing will run as part of the generation action. If tests fail, the generation action will fail and not push your SDK changes to main. # OpenAPI Data in Tests Source: https://speakeasy.com/docs/sdks/sdk-contract-testing/openapi-in-tests The definition of each operation determines what data is used in generated testing. In addition to the [data type system](/openapi/schemas) shaping data, OpenAPI Specification supports [examples](/openapi/examples). Test generation automatically uses defined examples when available. In the absence of defined examples, test generation attempts to use a realistic example based on the `type`, `format` (if set), and property name (if applicable). #### Example Property By default, a single test is created based on any `example` properties found throughout any defined operation `parameters`, `requestBody`, and `responses`. Consider the below example. The generator traverses the OpenAPI document to create a single test for the `updatePet` operation with `id`, `name`, and `photoUrls` data: ```yaml filename="openapi.yaml" paths: "/pet": put: tags: - pet summary: Update an existing pet description: Update an existing pet by Id operationId: updatePet requestBody: description: Update an existent pet in the store content: application/json: schema: "$ref": "#/components/schemas/Pet" required: true responses: "200": description: Successful operation content: application/json: schema: "$ref": "#/components/schemas/Pet" components: schemas: Pet: required: - name - photoUrls type: object properties: id: type: integer format: int64 example: 10 name: type: string example: doggie category: "$ref": "#/components/schemas/Category" photoUrls: type: array items: type: string tags: type: array items: "$ref": "#/components/schemas/Tag" status: type: string description: pet status in the store enum: - available - pending - sold ``` Here's how the generator parses this example document: - `paths[/pet].put.updatePet` - The test uses the `updatePet` operationId in the test name. - `paths[/pet].put.requestBody` - The test uses the `Pet` shared component for both the constructed request body and response object. - `components.schemas.Pet.required` - The `Pet` shared component is an object type with required `name` and `photoUrls` properties. - `components.schemas.Pet.id` - While not required, the `Pet` object `id` property has an `example` property, which is automatically included in the test. - `components.schemas.Pet.name` - The required `Pet` object `name` property has an `example` property, which is included in the test. - `components.schemas.Pet.photoUrls` - The required `Pet` object `photoUrls` property does not include an `example` property, however it has an example value automatically created since it is required. This definition creates a test with `Pet` object request body and response data: ```yaml id: 10 name: doggie photoUrls: - ``` #### Examples Property Multiple tests for an operation can be defined using the `examples` property, which in this context is a mapping of example name string keys to example values. Prevent missing or mismatched test generation by ensuring the same example name key is used across all necessary `parameters`, `requestBody`, and `responses` parts of the operation. If desired, [define reusable examples under components](/openapi/examples) similar to schemas. In this example, multiple tests (`fido` and `rover`) are created for the `addPet` operation: ```yaml filename="openapi.yaml" paths: "/pet": post: tags: - pet summary: Add a new pet to the store description: Add a new pet to the store operationId: addPet requestBody: description: Create a new pet in the store content: application/json: schema: "$ref": "#/components/schemas/Pet" examples: fido: summary: fido request description: fido example requestBody for test generation value: name: Fido photoUrls: - https://www.example.com/fido.jpg status: available rover: summary: rover request description: rover example requestBody for test generation value: name: Rover photoUrls: - https://www.example.com/rover1.jpg - https://www.example.com/rover2.jpg status: pending required: true responses: "200": description: Successful operation content: application/json: schema: "$ref": "#/components/schemas/Pet" examples: fido: summary: fido response description: fido example response for test generation value: id: 1 name: Fido photoUrls: - https://www.example.com/fido.jpg status: available rover: summary: rover response description: rover example response for test generation value: id: 2 name: Rover photoUrls: - https://www.example.com/rover1.jpg - https://www.example.com/rover2.jpg status: pending ``` #### Ignoring Data Data properties can be explicitly ignored in testing via the `x-speakeasy-test-ignore` annotation. In this example, the `other` property is omitted from test generation: ```yaml paths: /example: get: # ... other operation configuration ... responses: "200": description: OK content: application/json: schema: type: object properties: data: type: string other: type: string x-speakeasy-test-ignore: true ``` # Running SDK Tests Source: https://speakeasy.com/docs/sdks/sdk-contract-testing/running-tests import { Screenshot } from "@/mdx/components"; Run testing, via any of these options, depends on the desired use case: - Directly via the [`speakeasy test`](/docs/speakeasy-reference/cli/test) CLI command. - In [GitHub Actions workflows](/docs/sdk-testing/github-actions). - In the [`speakeasy run`](/docs/speakeasy-reference/cli/run) CLI command and existing GitHub Actions generation workflow with additional Speakeasy workflow configuration. ## Via CLI If you have multiple SDK targets the following will prompt you on which targets you would like to run tests for. ```bash speakeasy test ``` This will run tests for all SDKs in the repo. ```bash speakeasy test -t all ``` ## During Run For `speakeasy run` support, modify the Speakeasy workflow configuration (`.speakeasy/workflow.yaml`). Enable running tests during Speakeasy workflows by adding to one or more of the targets in the `targets` section of the configuration. ```yaml targets: example-target: # ... other existing configuration ... testing: enabled: true ``` ## Viewing Test Reports View test reports by navigating to the Tests tab under a particular SDK in the [speakeasy dashboard](https://app.speakeasy.com/). ## Next Steps - [Customize SDK tests](/docs/sdk-testing/customizing-sdk-tests) - [Setup testing in GitHub Actions](/docs/sdk-testing/github-actions) # Automated code sample URLs Source: https://speakeasy.com/docs/sdks/sdk-docs/code-samples/automated-code-sample-urls import { Callout, Screenshot } from "@/mdx/components"; import GetPublicUrlSnippet from "../integrations/_get-public-url-snippet.mdx"; To configure one of these combined code sample specs as a public URL for documentation providers, visit the **Docs** tab in the Speakeasy dashboard. ## How Speakeasy automates code sample URLs Speakeasy automatically tracks the base OpenAPI document and code samples when an SDK is generated using GitHub Actions and changes are merged to the main branch. Based on this, Speakeasy generates a combined spec in the background that contains all your existing OpenAPI operations along with any added `x-code-samples` extensions. ## Requirements for using automated code sample URLs If the SDK setup in GitHub is not yet complete, a notification like the following may appear in the **Docs** tab. To use automated code sample URLs, the SDK must meet the following requirements: - Each SDK's `workflow.yaml` file must include the following: - The `source` (the OpenAPI document) with a specified `registry` location. - The `target` (the SDK) with a `codeSamples` section that includes a specified `registry` location. - The SDK must be generated with GitHub Actions and merged to main. - The SDK GitHub Action must be in `direct` mode, or the `sdk-publish` action must be configured. While publishing to a package manager is _not_ necessary, release tagging must be handled by this action. - The base OpenAPI document must not include `x-codeSamples` extensions, as they will not be overwritten. When the setup is correct, the following will be available in the **APIs** tab of the Speakeasy dashboard: - An entry for the base OpenAPI document (for example, `my-source`). - For each SDK you include, a corresponding code samples overlay (for example, `my-source-{lang}-code-samples`). Revisions to the base OpenAPI document and code sample overlays must be tagged with `main`, which is why using GitHub Actions is required. If publishing setup is not ready, temporarily set up the following tagging action in each SDK repo. ```yaml filename=".github/workflows/tagging.yaml" name: Speakeasy Tagging permissions: checks: write contents: write pull-requests: write statuses: write "on": push: branches: - main paths: [] workflow_dispatch: {} jobs: tag: uses: speakeasy-api/sdk-generation-action/.github/workflows/tag.yaml@v15 with: registry_tags: main secrets: github_access_token: ${{ secrets.GITHUB_TOKEN }} speakeasy_api_key: ${{ secrets.SPEAKEASY_API_KEY }} ``` # The Code Samples API Source: https://speakeasy.com/docs/sdks/sdk-docs/code-samples/code-samples-api import { Callout } from "@/mdx/components"; This feature is for **Speakeasy Enterprise** customers. To inquire about access, please contact a Speakeasy representative, or [book a demo](/book-demo). ## Overview The Speakeasy Code Samples API is a streamlined solution for accessing rich, up-to-date SDK usage examples for Speakeasy managed SDKs. These examples can be easily integrated into an organization's documentation sites, tools, or developer portal, and they'll stay up to date automatically. This API is ideal for organizations who: - Use Speakeasy to generate SDKs from OpenAPI specifications, - Need reliable, up-to-date SDK usage examples for their APIs, and - Want custom tooling for SDK code samples in their documentation. ## Usage ### Prerequisites To use the Code Samples API, the following prerequisites are required: - A Speakeasy Enterprise Subscription, - An [Automated Code Sample URL](/docs/sdk-docs/code-samples/automated-code-sample-urls), configured for the desired Speakeasy SDK, and - A Speakeasy API Key for the workspace associated with the desired SDK if the OpenAPI Document is not publicly accessible. [Learn more about making OpenAPI Document public on Speakeasy.](/docs/sdk-docs/integrations/_get-public-url-snippet) ### TypeScript SDK The Code Samples SDK can be used in TypeScript projects to fetch snippets. The library also ships with some convenient features such as **React Query hooks**, and a **React Component**. For instructions on how to install and use the TypeScript SDK, refer to the [GitHub repo's README file](https://github.com/speakeasy-api/speakeasy-code-samples-ts/tree/main?tab=readme-ov-file#sdk-example-usage). #### The React Component This library includes a React component that fetches and highlights code snippets using `codehike`. Along with displaying the snippet, it shows a loading state during fetching and provides a fallback view if an error occurs. The component can be used as follows: ```tsx filename="App.tsx" import { SpeakeasyCodeSamplesCore } from "@speakeasyapi/code-samples/core"; import { CodeSampleFilenameTitle, CodeSamplesViewer, SpeakeasyCodeSamplesProvider, } from "@speakeasyapi/code-samples/react"; const speakeasyCodeSamples = new SpeakeasyCodeSamplesCore({ // optional if the registryUrl is publicly accessible apiKey: "", registryUrl: "https://spec.speakeasy.com/org/ws/my-source", }); function App() { return ( ); } ``` ## REST API Reference

Retrieve usage snippets

`GET /v1/code_sample` Retrieves usage snippets from an OpenAPI document stored in the registry. The endpoint supports filtering by programming language and operation ID.

Query Parameters

**`registry_url`** required, type: string - **Description**: The registry URL from which to retrieve the snippets - **Example**: `https://spec.speakeasy.com/my-org/my-workspace/my-source` **`operation_ids`** type: string[] - **Description**: The operation IDs to retrieve snippets for - **Example**: `getPets` **`method_paths`** type: {`{ method: string, path: string }[]`} - **Description**: The method paths to retrieve snippets for - **Example**: `[{"method": "get", "path": "/pets"}]` **`languages`** type: string[] - **Description**: The programming languages to retrieve snippets for - **Example**: `["python", "javascript"]`

Example Request

```shell filename="example-request.sh" curl -G "https://app.speakeasy.com/v1/code_sample" \ -H "X-Api-Key: " \ --data-urlencode "registry_url=https://spec.speakeasy.com/my-org/my-workspace/my-source" \ -d "languages=go" \ -d "languages=typescript" \ -d "operation_ids=getPets" ```

Example Response

```json filename="200 - Success" { "snippets": [ { "operationId": "getPetById", "path": "/pet/{id}", "method": "get", "language": "typescript", "code": "import { Petstore } from \"petstore-sdk\";\n\nconst petstore = new Petstore({\n apiKey: \"\",\n});\n\nasync function run() {\n const result = await petstore.pet.getById({\n id: 137396,\n });\n\n // Handle the result\n console.log(result);\n}\n\nrun();" } ] } ``` ```json filename="4XX - Error" { "status_code": 404, "message": "no snippets found for given operation IDs and languages -- err_not_found: not found" } ```

Generate usage snippets from example usage

`POST /v1/code_sample/from_example` Generates a single code snippet from the provided examples in an OpenAPI document. This endpoint optionally takes in a `speakeasy_spec_url` to specify which generation configuration to use. This is useful in circumstances where there are multiple configurations for the language target.

Request Body

**`oas`** required, type: object - **Description**: The OpenAPI specification document that defines the API - **Example**: A valid OpenAPI 3.x specification object **`request`** required, type: object - **Description**: The HTTP request example to use in the generated snippet - **Properties**: - `method`: HTTP method (GET, POST, etc.) - `url`: The request URL - `httpVersion`: HTTP version - `headers`: Array of request headers - `queryString`: Array of query parameters - `cookies`: Array of cookies - `headersSize`: Size of headers in bytes - `bodySize`: Size of body in bytes **`language`** required, type: string - **Description**: The target programming language for the generated SDK code - **Example**: `"typescript"`, `"python"`, `"go"`, `"java"` **`speakeasy_spec_url`** type: string - **Description**: Optional Speakeasy registry URL for additional context - **Example**: `"https://spec.speakeasy.com/my-org/my-workspace/my-source:1.0.0"`

Example Request

```shell filename="example-request.sh" curl -s -X POST https://app.speakeasy.com/v1/code_sample/from_example \ -H "Content-Type: application/json" \ -H "x-api-key: " \ -d '{ "oas": { "openapi": "3.0.2", "info": { "title": "Swagger Petstore - OpenAPI 3.0", "description": "This is a sample Pet Store Server based on the OpenAPI 3.0 specification.", "version": "1.0.0" }, "paths": { "/pet/{petId}": { "get": { "tags": ["pet"], "summary": "Find pet by ID", "description": "Returns a single pet", "operationId": "getPetById", "parameters": [ { "name": "petId", "in": "path", "description": "ID of pet to return", "required": true, "schema": { "type": "integer", "format": "int64" } } ], "responses": { "200": { "description": "successful operation", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Pet" } } } }, "400": { "description": "Invalid ID supplied" }, "404": { "description": "Pet not found" } }, "security": [ { "api_key": [] } ] } } }, "components": { "schemas": { "Pet": { "required": ["name", "photoUrls"], "type": "object", "properties": { "id": { "type": "integer", "format": "int64", "example": 10 }, "name": { "type": "string", "example": "doggie" }, "category": { "$ref": "#/components/schemas/Category" }, "photoUrls": { "type": "array", "items": { "type": "string" } }, "tags": { "type": "array", "items": { "$ref": "#/components/schemas/Tag" } }, "status": { "type": "string", "description": "pet status in the store", "enum": ["available", "pending", "sold"] } } }, "Category": { "type": "object", "properties": { "id": { "type": "integer", "format": "int64", "example": 1 }, "name": { "type": "string", "example": "Dogs" } } }, "Tag": { "type": "object", "properties": { "id": { "type": "integer", "format": "int64" }, "name": { "type": "string" } } } }, "securitySchemes": { "api_key": { "type": "apiKey", "name": "api_key", "in": "header" } } } }, "request": { "cookies": [], "headers": [ { "name": "api_key", "value": "special-key" } ], "headersSize": 35, "queryString": [], "bodySize": 0, "method": "GET", "url": "https://petstore3.swagger.io/api/v3/pet/10", "httpVersion": "HTTP/1.1" }, "language": "typescript", "speakeasy_spec_url": "https://spec.speakeasy.com/my-org/my-workspace/my-source:1.0.0" }' ```

Response

**Success Response (200)** Returns a single SDK code snippet for the matched operation: - **`code`**: The generated SDK code in the specified language - **`name`**: The operation name (typically the `operationId` from the OpenAPI spec) - **`install`**: Installation instructions for the SDK Installation instructions are not currently supported for `/v1/code_sample/from_example` calls. The `install` field will return "installation not yet supported". ```json filename="200 - Success" { "code": "import { Petstore } from \"petstore\";\n\nconst petstore = new Petstore({\n serverURL: \"https://api.example.com\",\n apiKey: process.env[\"PETSTORE_API_KEY\"] ?? \"\",\n});\n\nasync function run() {\n const result = await petstore.pet.getPetById({\n petId: 311674,\n });\n\n // Handle the result\n console.log(result);\n}\n\nrun();", "name": "getPetById", "install": "installation not yet supported" } ``` ```json filename="500 - Error" { "status_code": 500, "message": "failed to get generation config for speakeasy spec url -- no successful generation config found for language python and spec https://spec.speakeasy.com/my-org/my-workspace/my-source:1.0.0" } ``` # Generating code samples for your SDK Source: https://speakeasy.com/docs/sdks/sdk-docs/code-samples/generate-code-samples import { Callout } from "@/mdx/components"; This guide explains how code samples are generated for an SDK and how to apply them to an OpenAPI document. ## What is the code samples extension? Many API documentation providers provide code snippets in multiple languages to help developers understand how to use the API. However, these snippets may not correspond to a usage snippet from an existing SDK provided by the API, which reduces the value of the API documentation and can lead to inconsistent integrations, depending on whether a user discovers the API docs or the SDK first. The `x-codeSamples` extension (previously called `x-code-samples`) is a widely accepted OpenAPI Specification extension that enables the addition of custom code samples in multiple languages to operation IDs in an OpenAPI document. When custom code samples are added using the code samples extension, documentation providers will render the usage snippet in the right-hand panel of the documentation page: For a full breakdown of the code samples extension, see our [guide](/guides/openapi/x-codesamples). ## Configuring code samples Speakeasy provides code samples in the form of [overlays](/docs/prep-openapi/overlays/create-overlays#what-are-overlays). This ensures that your code samples can be trivially applied to your OpenAPI document without needing to upstream the changes. The setup for using overlays to apply code samples is configured in your workflow file, as follows: ```yaml filename=".speakeasy/workflow.yaml" # ... targets: my-target: target: typescript source: my-source codeSamples: output: code-samples.yaml # Optional, if you would like a local copy of your code samples to be produced registry: location: registry.speakeasy.com/my-org/my-workspace/my-source-typescript-code-samples blocking: false # Optional, defaults to true if not present ``` In the above example, a code samples overlay containing TypeScript usage snippets for all operations in the `my-source` OpenAPI document will be generated and written to `code-samples.yaml` and pushed to the Speakeasy registry. Speakeasy will automatically load code samples that are pushed to the registry and apply them to your OpenAPI document. This saves you from having to manually configure a workflow to integrate your code samples into your base document. Removing the `registry` section from your workflow file will disable this feature and require you to manually apply code samples to your OpenAPI document. ### Overrides To override the `lang` and `label` values, you can add either or both of the following options to the `codeSamples` section in your `workflow.yaml` file: ```yaml targets: my-target: codeSamples: # ... langOverride: # set `lang` to this value in all code samples labelOverride: omit: true # omit the label field entirely from the output # OR fixedValue: # set the label to this value in all code samples ``` ### Styles For certain documentation providers like ReadMe, you will need to override the default style of code samples in your OpenAPI document. To override the default style, modify the following option in the `codeSamples` section of your `workflow.yaml` file: ```yaml targets: my-target: codeSamples: # ... style: readme # Default is 'standard' ``` ## Automatic code sample URLs For paid accounts, Speakeasy provides an elegant solution for exposing code samples to documentation providers through its automated code sample URLs product. For a full breakdown of the feature, see the [guide](/docs/automated-code-sample-urls). ## Manually applying code samples to an OpenAPI document Alternatively, you can manually set up code sample integrations by pulling together code sample images in your repository with a simple workflow. ```yaml filename=".speakeasy/workflow.yaml" workflowVersion: 1.0.0 speakeasyVersion: latest sources: docs-source: inputs: - location: { { your_api_spec } } # local or remote references supported overlays: - location: registry.speakeasy.com///my-typescript-sdk-code-samples # location of the code samples from previous step - location: registry.speakeasy.com///my-go-sdk-code-samples - location: registry.speakeasy.com///my-python-sdk-code-samples output: openapi.yaml targets: {} ``` ```yaml filename=".github/workflows/sdk_generation.yaml" name: Generate permissions: checks: write contents: write pull-requests: write statuses: write "on": workflow_dispatch: inputs: force: description: Force generation of SDKs type: boolean default: false schedule: - cron: 0 0 * * * jobs: generate: uses: speakeasy-api/sdk-generation-action/.github/workflows/workflow-executor.yaml@v15 with: force: ${{ github.event.inputs.force }} mode: pr secrets: github_access_token: ${{ secrets.GITHUB_TOKEN }} speakeasy_api_key: ${{ secrets.SPEAKEASY_API_KEY }} ``` Now you can run `speakeasy run`. If you use registry references, the source and code samples will always be up to date with the main branch of your SDK repos. # Editing Your SDK Docs Source: https://speakeasy.com/docs/sdks/sdk-docs/edit-readme import { CodeWithTabs, Table } from "@/mdx/components"; Speakeasy-managed SDKs include a `README.md` file that contains at least the following sections by default: - `## Summary`: A brief introduction based on the `info` and `externalDocs` attributes defined in the OpenAPI document. - `## Table of Contents`: A list of links to all available sections in the README. - `## SDK Installation`: An installation snippet based on the package name provided in the `gen.yaml` file. - `## SDK Example Usage`: An example usage snippet based on the first operation in the OpenAPI document. - `## SDK Available Operations`: Links to documentation that covers all the SDK's methods. Here's what it looks like put together: ```markdown # github.com/client-sdk ## Summary For more information about the API: [](externalDocs.url) ## Table of Contents - [OpenAPI SDK](#openapi-sdk) - [SDK Installation](#sdk-installation) - [SDK Example Usage](#sdk-example-usage) - [Available Resources and Operations](#available-resources-and-operations) - ... - [Development](#development) - [Maturity](#maturity) - [Contributions](#contributions) ## SDK Installation ```bash go get github.com/client-sdk ``` ## SDK Example Usage ```go package main import ( "context" "fmt" "log" "os" "github.com/client-sdk" "github.com/client-sdk/pkg/models/shared" "github.com/client-sdk/pkg/models/operations" ) func main() { ctx := context.Background() opts := []sdk.SDKOption{ sdk.WithSecurity(shared.Security{ APIKey: shared.SchemeAPIKey{ APIKey: "YOUR_API_KEY", }, }), } s := sdk.New(opts...) res, err := s.ListPets(ctx) if err != nil { log.Fatal(err) } if res.Pets != nil { // handle response } } ``` ## SDK Available Operations - `ListPets` - List all pets You can enhance your README by adding content before or after any of the three main sections (**SDK Installation**, **SDK Example Usage**, and **SDK Available Options**). The generator will not overwrite any content you have added to the `README.md` file. The generator will automatically keep the content within the walled-off sections between the `` and `` comments, but the rest is up to you to maintain. If you would like to take over the management of automatically generated sections, you can do the following: 1. Remove the `` section comment. 2. Find the matching `` comment and change it to ``, which marks that section as managed by you. (This step is important. If you only remove the "Start" comment, the section may be re-inserted as described below.) 3. Edit the content between those comments as you see fit. If you change your mind at any time and want to go back to having Speakeasy manage a section, you can either delete the `` comment from the file, or replace it with ``, and the next generation will re-insert the Speakeasy-managed content into your file. Speakeasy may provide additional sections as new features are released or as you alter your SDK configuration by changing your OpenAPI specification and `gen.yaml` configuration. These new sections will be inserted above the comment named ``. (The placeholder heading will always be present in the file, and if you remove it, it will be added again just below the last README section.) Any missing sections will be inserted here during generation, so if you do not want a section inserted, be sure to follow the steps above to convert it to a `` section rather than removing it entirely. ## Usage Examples ### Main Usage section By default, `USAGE.md` and the **SDK Example Usage** section in the main `README.md` file will showcase a usage example from a random operation in the OpenAPI document. You can specify one or more operations to be used as the main usage example(s) by setting the `x-speakeasy-usage-example: true` extension to any operation in the OpenAPI document. The `x-speakeasy-usage-example` extension can be further configured to associate each usage snippet with a custom header, description, and relative positioning.
This may be particularly useful for guiding users through a specific set of instructions or a "getting started" section. For example: ```yaml paths: /pets: get: x-speakeasy-usage-example: title: List the pets description: Now get all of the pets previously added. position: 2 summary: List all pets operationId: ListPets tags: - pets responses: "200": description: OK content: application/json: schema: $ref: "#/components/schemas/Pets" put: x-speakeasy-usage-example: title: Add your pet description: First, add your own pet. position: 1 summary: Add pet operationId: AddPet tags: - pets requestBody: content: application/json: schema: $ref: "#/components/schemas/Pet" responses: "200": description: OK ``` This YAML will result in the following being added to the `README.md` and `USAGE.md` files: ```` ### Add your pet First, add your own pet. ```csharp using PetStore; using PetStore.Models.Pets; var sdk = new PetstoreSDK(); var req = new Pet(); var res = await sdk.Pets.AddPetAsync(req); ``` ### List the pets Now you can get all of the pets that have been added. ```csharp using PetStore; using PetStore.Models.Pets; var sdk = new PetstoreSDK(); var res = await sdk.Pets.ListPetsAsync(); ``` ```` ### Table of Contents To generate a more detailed Table of Contents, you can edit the value of the `$toc-max-depth` parameter in your `README.md` and subheadings will get updated upon regeneration: ```markdown - [openapi](#openapi) - [SDK Installation](#sdk-installation) - [SDK Example Usage](#sdk-example-usage) - [Example 1](#example-1) - [Example 2](#example-2) - [Example 3](#example-3) - [Custom Header](#custom-header) - ... ``` To ensure custom headings are properly referenced when manually adding sections to your `README.md` file, the basic markdown syntax should be respected: ```markdown ## Section Heading (level 2) ### Subsection Heading (level 3) ... ``` ### Feature sections Specific usage snippets can also be selected for other sections in the main `README.md`, provided they meet the requirements to showcase the feature at play. To do so, use the `x-speakeasy-usage-example` extension and specify a list of section `tags` (referring to `<-- Start Section [tag] -->` placeholders). For example, if you wish to select an operation for both the **Override Server URL Per-Operation** and the **Retries** sections, you can use the following: ```yaml /webhooks/subscribe: post: operationId: subscribeToWebhooks servers: - url: https://speakeasy.bar x-speakeasy-usage-example: tags: - server - retries x-speakeasy-retries: strategy: backoff backoff: initialInterval: 10 maxInterval: 200 maxElapsedTime: 1000 exponent: 1.15 ``` The supported `tags` and their associated conditions are listed in this table:
If you wish to select an operation for the main usage section as well as other sections, you can use the `usage` tag: ```yaml /drinks/{page}: get: operationId: listDrinks x-speakeasy-pagination: type: offsetLimit inputs: - name: page in: parameters type: page x-speakeasy-usage-example: title: "Browse available drinks" position: 2 tags: - usage - pagination ``` Note: The `title`, `description`, and `position` attributes only affect the main **SDK Example Usage** section. ### Values When generating usage examples, Speakeasy defaults to using any `example` values provided for schemas within your OpenAPI document. If no examples are present, Speakeasy will try to determine the most relevant example to generate from either the `format` field of a schema or the property name of a schema in an object. For example, if the schema has `format: email` or is within a property called `email`, Speakeasy will generate a random email address as an example value. ### Security Schemes For security schemes, the OpenAPI Specification does not allow you to specify examples of the values needed to populate the security details when initializing the SDKs. To provide custom examples for these values, add the `x-speakeasy-example` extension to the `securitySchemes` in your OpenAPI document. For example: ```yaml components: securitySchemes: apiKey: type: apiKey name: api_key in: header x-speakeasy-example: YOUR_API_KEY ``` The `x-speakeasy-example` value must be a string value and will be used as the example value for the Security Scheme. If the Security Scheme is a basic auth scheme, the example value will be a key-value pair that consists of a username and password split by a `;` character, such as `YOUR_USERNAME;YOUR_PASSWORD`. ## Comments ### Code Comments As part of the SDK generation, the Speakeasy CLI will generate comments for operations and models in generated SDKs. These comments are generated from the OpenAPI specification, based on the summary or description of the operation or schema. Comments are generated in the target language docstring format. For example, in Python, the comments will be generated as [PEP 257](https://www.python.org/dev/peps/pep-0257/)-compliant docstrings. By default, comments are generated for all operations and models. To disable comment generation for your SDK, modify your `gen.yaml` file to disable them, like so: ```yaml # ... generation: comments: disabled: true ``` ### Operation Comments Comments for each method in the generated SDK will be generated from the summary or description of the Operation. For example, if you have an Operation like the following: ```yaml paths: /pets: get: operationId: listPets summary: List all pets description: Get a list of all pets in the system responses: "200": description: A list of pets content: application/json: schema: type: array items: $ref: "#/components/schemas/Pet" ``` The generated SDK will have a method commented like so: operations.ListPetsResponse: r"""List all pets Get a list of all pets in the system """ # ...`, }, { label: "TypeScript", language: "typescript", code: ` /** * ListPets - List all pets * * Get a list of all pets in the system */ ListPets( config?: AxiosRequestConfig ): Promise { // ... }`, }, { label: "Java", language: "java", code: ` /** * ListPets - List all pets * * Get a list of all pets in the system **/ public ListPetsResponse listPets() { // ... }`, }, { label: "C#", language: "csharp", code: `/// /// List all pets /// /// /// Get a list of all pets in the system /// /// Task ListPetsAsync();`, }, ]} /> If both the summary and description are present, the summary will be used as the first line of the comment and the description will be used as the second line of the comment. If just the description is present, it will be used as the first line of the comment. If both are present, but you would like to omit the description as it might be too verbose, you can use the `omitdescriptionifsummarypresent` option in your `gen.yaml` file, as follows: ```yaml # ... generation: comments: omitDescriptionIfSummaryPresent: true ``` ### Model Comments For each model in the generated SDK, comments are generated from the description of the schema. For example, if you have the following schema: ```yaml components: schemas: Pet: type: object description: A pet sold in the pet store properties: id: type: integer format: int64 name: type: string ``` The generated SDK will have a model commented like so: /// A pet sold in the pet store /// public class Pet { // ... }`, }, ]} /> ### Per-SDK Comments You can configure comments that only display in the SDK for a single language. For example, if you need the comment for the TypeScript or the Golang SDK to say something different from the others, or you want to control the documentation separately for each language, you can use the Speakeasy `x-speakeasy-docs` extension. Anywhere you can set the `summary` or `description`, you can also add `x-speakeasy-docs` with per-language text for the docs. Consider the following parameter description: ```yaml parameters: - name: type in: query description: This query parameter names the type of drink to filter the results by. If not provided, all drinks will be returned. required: false schema: $ref: "#/components/schemas/DrinkType" x-speakeasy-docs: go: description: The type field names the type of drink to filter the results by. If set to nil, all drinks will be returned. python: description: The ``type`` field names the type of drink to filter the results by. If set to ``None``, all drinks will be returned. typescript: description: This field is the type of drink to filter the results by. If set to null, all drinks will be returned. ``` The documentation generated for each SDK will contain different comments specific to the respective programming languages. ## Class Names By default, Speakeasy SDKs will be generated with the class name, `SDK`. However, you can configure a custom class name by modifying your `gen.yaml` file to include: ```yaml generation: sdkClassName: "myClassName" ``` This will yield a package like this: ```go package petshop import ( "net/http" "openapi/pkg/utils" ) var ServerList = []string{ "http://petstore.speakeasy.io/v1", } type HTTPClient interface { Do(req *http.Request) (*http.Response, error) } type PetShop struct { Pets *Pets _defaultClient HTTPClient _securityClient HTTPClient _serverURL string _language string _sdkVersion string _genVersion string } ``` # _get-public-url-snippet Source: https://speakeasy.com/docs/sdks/sdk-docs/integrations/_get-public-url-snippet {/* This snippet is made to be reused in docs that require instruction on grabbing the Public URL. */} import { Screenshot } from "@/mdx/components"; ### Get the API's combined spec public URL from the registry Navigate to the [Speakeasy Dashboard](https://app.speakeasy.com) and click on the **Docs** tab. Select the **Integrate with a Docs Provider** option. On the **Integrate with a Docs Provider** page, select the correct OpenAPI document using the **Change OpenAPI Documents** button. Then, copy the Spec URL. Make sure the **Public** toggle is enabled, so Mintlify can access the OpenAPI document. # Integrate Speakeasy with Bump.sh Source: https://speakeasy.com/docs/sdks/sdk-docs/integrations/bump import { Screenshot } from "@/mdx/components"; import GetPublicUrlSnippet from "../integrations/_get-public-url-snippet.mdx"; --- Bump.sh is a hosted solution for simple API documentation, API catalogs, and API explorers, which makes it a great tool to use in conjunction with [Speakeasy's Automated Code Samples feature](/docs/sdk-docs/code-samples/automated-code-sample-urls). Embed SDKs right into the API documentation, making it easier for developers to get started with the API. ## Prerequisites To integrate Bump.sh with Speakeasy, you'll need the following: - A Bump.sh account. Sign up at [https://bump.sh/users/sign_up](https://bump.sh/users/sign_up). - A Speakeasy-generated SDK with a configured [automated code sample URL](/docs/sdk-docs/code-samples/automated-code-sample-urls). ## Setting up the integration ### Import the combined spec URL into Bump.sh In the Bump.sh dashboard, either create **New Documentation** or open existing API documentation. Click **Settings** and open the **Automatic Deployment** tab. Select whether you want to deploy via GitHub Actions or CLI, and copy the appropriate example, which will include the Doc ID and the API token for you. If you're new to Bump.sh, start with the CLI. Open a terminal and run the following command: ```bash npm install -g bump-cli bump deploy https://spec.speakeasy.com/walker/walker/book-club-oas-with-code-samples --token= --doc= ``` When the import is complete, the API documentation will be rendered, and Speakeasy-generated code samples will be embedded in the relevant OpenAPI operations. ## Next steps For more advanced configurations than this basic setup, refer to the [Bump.sh and Speakeasy integration guide](https://docs.bump.sh/guides/bump-sh-tutorials/generate-sdks-with-speakeasy/), which demonstrates using GitHub Actions to automate deployments. Bump.sh is more than just an OpenAPI renderer – it also provides API catalogs, discovery tools, and interactive playgrounds. Learn more about Bump.sh in the [official documentation](https://docs.bump.sh/). # Integrate Speakeasy with Mintlify Source: https://speakeasy.com/docs/sdks/sdk-docs/integrations/mintlify import { Screenshot } from "@/mdx/components"; import GetPublicUrlSnippet from "../integrations/_get-public-url-snippet.mdx"; --- Autogenerated code snippets from Speakeasy SDKs can be integrated directly into Mintlify API reference documentation. SDK usage snippets are shown in the [interactive playground](https://mintlify.com/docs/api-playground/overview) of Mintlify-powered documentation sites. ## Prerequisites To integrate Mintlify with Speakeasy, you'll need the following: - A [Mintlify documentation repository](https://mintlify.com/docs/quickstart#creating-the-repository). - A Speakeasy-generated SDK with a configured [automated code sample URL](/docs/sdk-docs/code-samples/automated-code-sample-urls). ## Setting up the integration ### Update the configuration file Add the combined spec URL to **Anchors** or **Tabs** in the `docs.json` configuration file of a Mintlify repository. #### Using docs.json Add the combined spec URL to **Anchors** by updating the `anchor` field in the `docs.json` file as follows: ```json filename="docs.json" { "anchors": [ { "name": "API Reference", "url": "api-reference", "icon": "square-terminal", "openapi": "SPEAKEASY_COMBINED_SPEC_URL" } ] } ``` Add the combined spec URL to **Tabs** by updating the `tab` field in the `docs.json` file as follows: ```json filename="docs.json" { "tabs": [ { "name": "API Reference", "url": "api-reference", "openapi": "SPEAKEASY_COMBINED_SPEC_URL" } ] } ``` #### Migrating from mint.json If you're using the legacy `mint.json` configuration file, upgrade to `docs.json` using the Mintlify CLI: ```bash mint upgrade ``` This command creates a `docs.json` file from your existing `mint.json`. After verifying the configuration, delete the old `mint.json` file. Speakeasy-generated code snippets can now be viewed in the Mintlify API reference documentation. See the code snippets in action in the [interactive playground](https://mintlify.com/docs/api-playground/overview) of the Mintlify-powered documentation site. ## Next steps Mintlify offers flexible customization options for API references generated from OpenAPI documents. For more information, refer to the [Mintlify documentation](https://mintlify.com/docs/api-playground/openapi/setup). # Integrate Speakeasy with ReadMe Source: https://speakeasy.com/docs/sdks/sdk-docs/integrations/readme import { Callout, Screenshot } from "@/mdx/components"; import GetPublicUrlSnippet from "../integrations/_get-public-url-snippet.mdx"; --- Autogenerated code snippets from Speakeasy SDKs can be integrated directly into a ReadMe documentation site. ## Prerequisites To integrate ReadMe with Speakeasy, the following is required: - A [ReadMe project created](https://docs.readme.com/main/docs/quickstart-guide#step-1-creating-your-project). - A Speakeasy-generated SDK with a configured [automated code sample URL](/docs/sdk-docs/code-samples/automated-code-sample-urls). ## Setting up the integration ### Configure `workflow.yaml` for ReadMe To display code samples from Speakeasy SDKs in ReadMe, update the `workflow.yaml` file to support their proprietary OpenAPI extension. In the SDK target configuration, set the `codeSamples.style` field to `readme`: ```yaml filename=".speakeasy/workflow.yaml" targets: my-target: target: typescript codeSamples: # !mark(1:2) style: readme langOverride: javascript # see note below registry: location: registry.speakeasy.com/... blocking: false ... ``` If the `target` value is set to `typescript`, the `langOverride` field must be set to `javascript` or generated code samples will **not** be displayed in ReadMe. {` `} If `target` is **not** set to `typescript`, then `langOverride` should be set to `auto-detect`. ### Upload the combined spec URL to ReadMe In the [ReadMe dashboard](https://dash.readme.com), open the project. Click the **API Reference** tab, then click **+ New API Definition**. Paste the combined spec URL from Speakeasy into the text input below **Import from URL**, then click **Import**. Speakeasy-generated code snippets are now available in the ReadMe project's **API Reference** section. ## Next steps For help customizing ReadMe API references generated from OpenAPI documents, see the [ReadMe OpenAPI support documentation](https://docs.readme.com/main/docs/openapi). # Integrate Speakeasy with Scalar Source: https://speakeasy.com/docs/sdks/sdk-docs/integrations/scalar import { Screenshot } from "@/mdx/components"; import GetPublicUrlSnippet from "../integrations/_get-public-url-snippet.mdx"; --- **Scalar** enables easy creation and maintenance of API documentation. Scalar renders documentation by referencing a live OpenAPI document, making it compatible with [Speakeasy's Automated Code Samples feature](/docs/sdk-docs/code-samples/automated-code-sample-urls). ## Prerequisites To integrate Scalar with Speakeasy, you'll need the following: - A Scalar account. Sign up at [https://dashboard.scalar.com/register](https://dashboard.scalar.com/register). - A Speakeasy-generated SDK with a configured [automated code sample URL](/docs/sdk-docs/code-samples/automated-code-sample-urls). ## Setting up the integration ### Import the combined spec URL into Scalar Open a [Scalar project](https://docs.scalar.com), go to the **Reference** tab, and click **Import URL**. Paste the combined spec URL copied from Speakeasy into the provided field, optionally check **Create Link**, then click **Import**. When the import is complete, the API documentation will be rendered. ## Next steps Scalar is more than just an OpenAPI renderer. Visit the [official Scalar documentation](https://guides.scalar.com/scalar/introduction) to learn more. # Integrate via React Source: https://speakeasy.com/docs/sdks/sdk-docs/snippet-ai/integrate-via-react import { CodeWithTabs, Table } from "@/mdx/components"; SnippetAI is a React package designed to simplify generating code samples directly from OpenAPI specifications, enabling developers to rapidly prototype and integrate APIs into their applications. ### Installation To install SnippetAI in a React project, install it using the package manager of choice: ### Usage After installation, import and utilize SnippetAI in React components. An active publishing token is required. Generate one in the settings page for the organization. ```jsx import { SnippetAI } from "@speakeasy-api/snippet-ai-react"; function MyComponent() { return ; } export default MyComponent; ``` ### Supported Languages SnippetAI currently supports code generation for the following languages:
### Props The `SnippetAI` component accepts the following props:
### Language Selection If a `codeLang` prop is not provided, a dropdown will be displayed to select the target language for the generated snippets. ![Screenshot of SnippetAI UI with language selector](/assets/docs/snippet-ai/snippet-ai-language-select.png) # Integrate via Script Source: https://speakeasy.com/docs/sdks/sdk-docs/snippet-ai/integrate-via-script import { Table } from "@/mdx/components"; Install the SnippetAI web component via a script tag. This is useful for doc providers that don't allow adding custom script tags but do allow custom JS. For React environments or when custom script tags are available, refer to [Integrate via React](/docs/sdk-docs/snippet-ai/integrate-via-react) or [Integrate via Web Component](/docs/sdk-docs/snippet-ai/integrate-via-web-component) which are preferred. ## Required Steps Add the two components to your documentation site; 1. A button with an HTML ID that you want to trigger the component 2. The script tag to mount the web component Below is an example minimal HTML installation. ```html ``` In the above script you can configure your component by adjusting the following HTML attributes
# Integrate via Web Component Source: https://speakeasy.com/docs/sdks/sdk-docs/snippet-ai/integrate-via-web-component import { CodeWithTabs, Table } from "@/mdx/components"; Install SnippetAI as a web component to enhance documentation with AI-powered code generation. If the environment doesn't support custom script tags, [Integrate via a Script](/docs/sdk-docs/snippet-ai/integrate-via-script) ## Required Setup Add one of the following script tags to the HTML file's `` section, using the appropriate module format for the site. Choose the ES Module version for modern web applications to leverage performance. Select the UMD version for older browsers and traditional script loading. Add the script tag before any code that uses the `` element to ensure proper loading and registration. Add this to the HTML file: `, }, { label: "UMD", language: "html", code: ``, }, ]} /> After installing the web component, add the `` element to the HTML. The component accepts the following attributes:
The component will automatically initialize and display the code generator interface when added to the page. ```html ``` ### Custom Trigger To have a custom trigger button, add a unique element ID the HTML element to open SnippetAI as property to the web component and the element. ```html ``` ### Language Selection If a `codelang` attribute is not provided, a dropdown will be displayed to select the target language for the generated snippets. ![Screenshot of SnippetAI UI with language selector](/assets/docs/snippet-ai/snippet-ai-language-select.png) # SnippetAI Overview Source: https://speakeasy.com/docs/sdks/sdk-docs/snippet-ai/overview import { Screenshot } from "@/mdx/components"; ![Screenshot of SnippetAI's chat interface](/assets/docs/snippet-ai/snippet-ai.png) Add SnippetAI to web applications as a web component to help users generate code snippets with AI-powered tools. ## How It Works SnippetAI offers an intuitive interface for code snippet generation. Users type questions about API usage, and the component responds with ready-to-use code examples that leverage the actual SDK and code samples. SnippetAI stands out by using the existing SDK, which helps users discover the API's capabilities. ![Screenshot of SnippetAI with generated code snippets](/assets/docs/snippet-ai/snippet-ai-demo.png) For example, when a user submits the query: "How do I create a new user with the API?" The component analyzes the OpenAPI specification and SDK code samples to generate a function that answers their question, matching the actual implementation. ## Prerequisites Before implementing SnippetAI, complete the following configuration steps: 1. Enable the SnippetAI add-on in documentation settings: - Navigate to the Docs tab in the Speakeasy dashboard - Select the SnippetAI card 2. Activate SnippetAI using the toggle switch. The indicator will display green when SnippetAI is enabled 3. Generate and configure a publishing token: - Select "New" to access the token configuration interface - Provide a descriptive identifier for the token - Select the target SDK - Define the SDK tag - Select "Generate" to create the token 4. Integrate Snippet AI into the docsite - Upon completion, the site displays an integration guide with the publishing token and a quick start guide - Further details below! ## Integration Options ### React Implement SnippetAI as a React component to integrate with React-based documentation. ### Web Component Implement SnippetAI as a web component by selecting one of two script integration methods: - **ES Module**: Recommended for modern web applications, providing optimal tree-shaking and module support - **UMD**: Suitable for legacy browsers and traditional script loading implementations Include the selected script tag in the HTML file's `` section before implementing the `` element. ## Usage Once integrated into documentation, users can invoke the SnippetAI interface via the `Generate Code Example` button or the keyboard shortcut `⌘+S` / `Ctrl+S` to: - Ask natural language questions about how to use the API - Receive contextually relevant code snippets that use the actual SDKs - Copy generated code directly into their applications to rapidly decrease time-to-integration # Using Speakeasy with Docker Source: https://speakeasy.com/docs/speakeasy-reference/cli/docker The Speakeasy Docker image provides a containerized environment for running Speakeasy CLI commands. The image does not include language toolchains, so it cannot be used for SDK generation. Use it for other tasks like validating OpenAPI specs, checking SDK status, and running quickstart commands. ## Docker image The official Speakeasy Docker image is available on GitHub Container Registry: [ghcr.io/speakeasy-api/speakeasy](https://github.com/speakeasy-api/speakeasy/pkgs/container/speakeasy) ## Installation Pull the latest Speakeasy Docker image: ```bash docker pull ghcr.io/speakeasy-api/speakeasy:latest ``` You can also use specific version tags instead of `latest` for reproducible builds: ```bash docker pull ghcr.io/speakeasy-api/speakeasy:v1.234.0 ``` ## Authentication To authenticate with the Speakeasy Platform from within a Docker container, you need to: 1. Set the `SPEAKEASY_LOGIN_CALLBACK_PORT` environment variable 2. Expose the callback port for OAuth authentication 3. Mount your workspace directory ```bash docker run -it -v "$(pwd):/workspace" -w "/workspace" \ -e SPEAKEASY_LOGIN_CALLBACK_PORT=$SPEAKEASY_LOGIN_CALLBACK_PORT \ -p $SPEAKEASY_LOGIN_CALLBACK_PORT:$SPEAKEASY_LOGIN_CALLBACK_PORT \ ghcr.io/speakeasy-api/speakeasy:latest auth login ``` After running this command, a browser window will open for you to authenticate with the Speakeasy Platform. ### Using API keys in CI/CD For automated environments like CI/CD pipelines, use an API key instead of interactive authentication: ```bash docker run -it -v "$(pwd):/workspace" -w "/workspace" \ -e SPEAKEASY_API_KEY=$SPEAKEASY_API_KEY \ ghcr.io/speakeasy-api/speakeasy:latest run ``` Create an API key in the [Speakeasy Platform](https://app.speakeasy.com) and set it as the `SPEAKEASY_API_KEY` environment variable. ## Common usage examples ### Run quickstart Get started with an interactive quickstart: ```bash docker run -it -v "$(pwd):/workspace" -w "/workspace" \ ghcr.io/speakeasy-api/speakeasy:latest quickstart ``` ### Validate OpenAPI spec Validate your OpenAPI specification: ```bash docker run -it -v "$(pwd):/workspace" -w "/workspace" \ ghcr.io/speakeasy-api/speakeasy:latest validate openapi -s ./openapi.yaml ``` ### Check SDK status Check the status of your SDK generation: ```bash docker run -it -v "$(pwd):/workspace" -w "/workspace" \ ghcr.io/speakeasy-api/speakeasy:latest status ``` ## Using in CI/CD pipelines The Docker image is designed for use in CI/CD pipelines. Here's an example GitHub Actions workflow for validating an OpenAPI spec: ```yaml name: Validate OpenAPI on: push: branches: [main] jobs: validate: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - name: Validate OpenAPI spec run: | docker run -v "$(pwd):/workspace" -w "/workspace" \ ghcr.io/speakeasy-api/speakeasy:latest validate openapi -s ./openapi.yaml ``` ## Volume mounts and permissions When using the Docker image, ensure that: 1. Your workspace directory is mounted to `/workspace` 2. The working directory is set to `/workspace` with `-w "/workspace"` 3. File permissions allow the container to read and write files If you encounter permission issues, you may need to adjust file ownership or run the container with appropriate user permissions. ## Troubleshooting ### Authentication issues If authentication fails, ensure that: - The callback port is properly exposed with `-p` - The `SPEAKEASY_LOGIN_CALLBACK_PORT` environment variable is set - Your firewall allows connections on the callback port ### File permission errors If file permission errors occur, try running the container with the appropriate user ID: ```bash docker run -it --user $(id -u):$(id -g) \ -v "$(pwd):/workspace" -w "/workspace" \ ghcr.io/speakeasy-api/speakeasy:latest validate openapi -s ./openapi.yaml ``` ### Network connectivity Ensure the Docker container has network access to: - The Speakeasy Platform (app.speakeasy.com) - The OpenAPI specification source (if remote) # Getting Started Source: https://speakeasy.com/docs/speakeasy-reference/cli/getting-started The Speakeasy CLI provides access to features of the Speakeasy Platform. This CLI supports an interactive mode. Simply type `speakeasy` in the terminal for a guided set-up and usage experience. ## Install In the terminal, run: ### Homebrew (macOS) ```bash brew install speakeasy-api/tap/speakeasy ``` ### Script (macOS and Linux) ```bash curl -fsSL https://go.speakeasy.com/cli-install.sh | sh ``` ### winget (Windows) ```cmd winget install speakeasy ``` ### Chocolatey (Windows) ```cmd choco install speakeasy ``` ### Manual Installation Download the latest Speakeasy CLI release for the appropriate platform from the [releases page](https://github.com/speakeasy-api/speakeasy/releases), extract, and add the binary to the path. ## Authenticate Authenticate with the Speakeasy Platform to use the Speakeasy CLI: ```bash speakeasy auth login ``` A browser window will open. Log in to the Speakeasy Platform and create a workspace (or select a workspace if you have previously used the platform) by following the prompts. When redirected to the workspace, Speakeasy will generate an API key. Return to the terminal to see a message confirming authentication. ## Run Get started with the installed Speakeasy CLI with a single interactive command. ```bash speakeasy quickstart ``` Getting started is that easy. For the full set of CLI commands, type `speakeasy -h`. ## Using the Speakeasy CLI in CI/CD To use the Speakeasy CLI in a CI/CD pipeline, authenticate it by creating an API key in the [Speakeasy Platform](https://app.speakeasy.com) and then set the `SPEAKEASY_API_KEY` environment variable to the value of an API key from the API keys page. # Using Speakeasy CLI with mise toolkit Source: https://speakeasy.com/docs/speakeasy-reference/cli/mise-toolkit import { Callout, CodeWithTabs } from "@/mdx/components"; [mise](https://mise.jdx.dev/) (pronounced "MEEZ ahn plahs") is a polyglot tool version manager that serves as the front-end to your dev environment. It can manage development tools, environments, and tasks, replacing tools like asdf, nvm, pyenv, rbenv, and others. This guide shows how to install and manage the Speakeasy CLI using mise toolkit. ## Prerequisites First, you need to install mise on your system. Follow the [mise installation guide](https://mise.jdx.dev/installing-mise.html) for your operating system. After installation, add mise to your shell configuration: ```bash echo 'eval "$(mise activate bash)"' >> ~/.bashrc # or for zsh echo 'eval "$(mise activate zsh)"' >> ~/.zshrc ``` ## Install Speakeasy CLI with mise Once mise is installed and configured, you can install the Speakeasy CLI using one of the available backends: - **aqua**: Recommended backend that offers the most features and security - **ubi**: Universal Binary Installer, simpler but with fewer features Both backends will install the same Speakeasy CLI functionality. ## Verify Installation After installation, verify that Speakeasy CLI is available: ```bash speakeasy --version ``` You should see the version number of the installed Speakeasy CLI. ## Managing Versions ### Install a Specific Version ```bash # Install a specific version using aqua backend mise use aqua:speakeasy-api/speakeasy@1.556.2 # Or using ubi backend mise use ubi:speakeasy-api/speakeasy@1.556.2 ``` ### List Available Versions ```bash # List available versions mise ls-remote speakeasy ``` ### Update to Latest Version ```bash # Update to the latest version mise use speakeasy@latest ``` ## Project-Specific Configuration You can configure Speakeasy CLI versions per project using mise's configuration files: ### Using .mise.toml Create a `.mise.toml` file in your project root: ```toml [tools] speakeasy = "1.556.2" ``` ### Using .tool-versions Alternatively, use the `.tool-versions` format: ``` speakeasy 1.556.2 ``` When you enter the project directory, mise will automatically use the specified version. ## Authentication and Usage After installing Speakeasy CLI with mise, authenticate and start using it: ```bash # Authenticate with Speakeasy Platform speakeasy auth login # Start with quickstart speakeasy quickstart ``` For the full set of CLI commands, type `speakeasy -h`. ## Using in CI/CD To use Speakeasy CLI with mise in CI/CD pipelines: ```yaml # Example GitHub Actions workflow - name: Install mise uses: jdx/mise-action@v2 env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - name: Install Speakeasy CLI run: mise use aqua:speakeasy-api/speakeasy - name: Use Speakeasy CLI run: speakeasy quickstart env: SPEAKEASY_API_KEY: ${{ secrets.SPEAKEASY_API_KEY }} ``` ## Troubleshooting ### Command Not Found If `speakeasy` command is not found after installation: 1. Ensure mise is properly activated in your shell 2. Restart your terminal or run `source ~/.bashrc` (or `~/.zshrc`) 3. Verify the installation with `mise list` ### Version Conflicts If you have multiple version managers installed: 1. Check which tools are managing Speakeasy: `which speakeasy` 2. Ensure mise takes precedence in your PATH 3. Consider uninstalling other version managers to avoid conflicts ## Next Steps - [Generate your first SDK](/docs/sdks/create-client-sdks) - [Customize your SDK](/docs/customize-sdks/) - [Set up CI/CD pipeline](/docs/speakeasy-reference/generation/ci-cd-pipeline) # List of Speakeasy extensions Source: https://speakeasy.com/docs/speakeasy-reference/extensions import { Table } from "@/mdx/components";
You can use `x-speakeasy-extension-rewrite` to map any extension from the wider OpenAPI ecosystem or another vendor to the equivalent Speakeasy extension. This allows you to use your existing OpenAPI spec without needing to make changes to it, if necessary. ```yaml openapi: 3.1.0 info: title: My API version: 1.0.0 x-speakeasy-extension-rewrite: x-speakeasy-enums: x-enum-varnames # Maps x-enum-varnames used by the OSS generator to x-speakeasy-enums which has the same functionality ``` ## Terraform-specific extensions
# Workflow matrix Source: https://speakeasy.com/docs/speakeasy-reference/generation/ci-cd-pipeline import { Callout, Table } from "@/mdx/components"; To quickly set up the workflow, run `speakeasy configure github` in the root of the SDK repository. This automates the setup and commits the necessary files. For more complex or custom configurations, the following is supported. ## Workflow inputs ```yml "on": workflow_dispatch: inputs: force: description: Force generation of SDKs type: boolean default: false set_version: description: optionally set a specific SDK version type: string runs-on: description: Runner to use for the workflow (e.g., large-ubuntu-runner) type: string default: ubuntu-latest ``` The inputs to the workflow determine how the SDKs will be generated.
## Workflow jobs The generate job utilizes the Speakeasy SDK generation action. It references the `workflow-executor.yaml` from the `sdk-generation-action` repo, which handles the core operations like pulling the OpenAPI document, validating it, and generating the SDKs. ### With ```yml jobs: generate: uses: speakeasy-api/sdk-generation-action/.github/workflows/workflow-executor.yaml@v15 with: force: ${{ github.event.inputs.force }} mode: pr set_version: ${{ github.event.inputs.set_version }} speakeasy_version: latest github_repository: acme-org/acme-sdk-typescript runs-on: ${{ github.event.inputs.runs-on }} ```
### Secrets ```yml secrets: github_access_token: ${{ secrets.GITHUB_TOKEN }} npm_token: ${{ secrets.NPM_TOKEN }} speakeasy_api_key: ${{ secrets.SPEAKEASY_API_KEY }} ```
## Workflow outputs The workflow provides outputs that indicate which SDKs were regenerated and can trigger further actions in the pipeline, such as validating, testing, and publishing the SDKs.
# C# configuration options Source: https://speakeasy.com/docs/speakeasy-reference/generation/csharp-config import { Table } from "@/mdx/components"; This section details the available configuration options for the C# SDK. All configuration is managed in the `gen.yaml` file under the `csharp` section. ## Version and general configuration ```yml csharp: version: 1.2.3 author: "Author Name" packageName: "custom-sdk" dotnetVersion: "net8.0" description: "A description of your SDK" ```
## Publishing configuration ```yml csharp: packageTags: "openapi sdk rest" includeDebugSymbols: true enableSourceLink: true ```
## Additional dependencies ```yml csharp: additionalDependencies: - package: Newtonsoft.Json version: 13.0.3 ```
## Method and parameter management ```yml csharp: maxMethodParams: 4 enableCancellationToken: true ```
## Security configuration ```yml csharp: flattenGlobalSecurity: true ```
## Module management ```yml csharp: sourceDirectory: "src" disableNamespacePascalCasingApr2024: false ```
## Import management ```yml csharp: imports: option: "openapi" paths: callbacks: models/callbacks errors: models/errors operations: models/operations shared: models/components webhooks: models/webhooks ```
### Import paths
## Error and response handling ```yml csharp: clientServerStatusCodesAsErrors: true responseFormat: "envelope-http" ```
# The gen.yaml file reference Source: https://speakeasy.com/docs/speakeasy-reference/generation/gen-yaml import { Callout } from "@/mdx/components"; For most use cases, the `speakeasy configure` command is the recommended means of interacting with the Speakeasy `gen.yaml` file. The `speakeasy configure` command has subcommands for configuring sources, targets, GitHub workflow setups, and package publications. All new targets created using `speakeasy quickstart` automatically generate workflow files in the `.speakeasy/` folder in the root of the target directory. The `gen.yaml` file has several sections: - The `generation` section is essential for SDK configuration - The `management` and `features` sections are maintained by Speakeasy and should not be edited - The final section is for language-specific configuration (for more information, see the language-specific configuration docs) ## Generation ### configVersion The currently supported version of the Speakeasy `gen.yaml` configuration file is `2.0.0`. Older versions will be automatically upgraded when encountered. ```yaml configVersion: 2.0.0 ``` ### generation The `generation` section of the `gen.yaml` file supports configuration that is relevant to all SDK targets. If a value isn't configured here, and it has a default value, then that value will be added automatically on the next generation. For more information about SDK generation and targets, see our [core concepts documentation](/docs/sdks/core-concepts#sdk-generation). ```yaml generation: sdkClassName: speakeasybar maintainOpenAPIOrder: true usageSnippets: optionalPropertyRendering: withExample devContainers: enabled: true schemaPath: "path/to/schema" useClassNamesForArrayFields: true fixes: nameResolutionDec2023: true parameterOrderingFeb2024: true requestResponseComponentNamesFeb2024: true securityFeb2025: true auth: OAuth2ClientCredentialsEnabled: true inferSSEOverload: true ``` ### sdkClassName Defines the class name of the main imported class in the generated SDK. ```yaml sdkClassName: speakeasybar ``` ### maintainOpenAPIOrder Determines whether the parameters, properties, operations, etc., are maintained in the same order they appear in the OpenAPI document. If set to `false`, these elements are sorted alphabetically. ```yaml maintainOpenAPIOrder: true ``` ### usageSnippets The options for `optionalPropertyRendering` include `always`, `never`, and `withExample`, which renders optional properties only when an example is present in the OpenAPI document. ```yaml usageSnippets: optionalPropertyRendering: withExample ``` ### devContainers Enables or disables the use of development containers, and specifies the schema path. For more information about development containers and SDK sandboxes, see our [SDK sandbox documentation](/docs/manage/sdk-sandbox). ```yaml devContainers: enabled: true schemaPath: "path/to/schema" ``` ### useClassNamesForArrayFields When set to true, array fields use class names instead of child schema types. ```yaml useClassNamesForArrayFields: true ``` ### fixes Includes specific fixes or features to be applied during SDK generation to avoid breaking changes. - `nameResolutionDec2023`: **Disabling not recommended**. Enables changes introduced in December 2023 for improved name resolution, defaults to `true` for new SDKs. For older SDKs, setting `true` is recommended, but will be a breaking change. - `parameterOrderingFeb2024`: **Disabling not recommended**. Enables changes introduced in February 2024 to respect the order of parameters in the OpenAPI document where possible, defaults to `true` for new SDKs. For older SDKs, setting `true` is recommended, but will be a breaking change. - `requestResponseComponentNamesFeb2024`: **Disabling not recommended**. Enables changes introduced in February 2024 to use the name of parent request/response components where possible, defaults to `true` for new SDKs. For older SDKs, setting `true` is recommended, but will be a breaking change. - `securityFeb2025`: **Disabling not recommended**. Enables changes introduced in February 2025 to the security handling at both the global and operation level, particularly needed to enable per-operation OAuth2 flows. Defaults to `true` for new SDKs. For older SDKs, setting `true` is recommended, but will be a breaking change. ```yaml fixes: nameResolutionDec2023: true parameterOrderingFeb2024: true requestResponseComponentNamesFeb2024: true securityFeb2025: true ``` ### auth - `OAuth2ClientCredentialsEnabled`: Enables the generation of code for handling OAuth 2.0 client credentials for authentication, where possible. **Business and Enterprise plans only**. - `hoistGlobalSecurity`: When `true` (default), Speakeasy identifies the most commonly used operation-level security scheme and hoists it to global security if no global security is defined. Set to `false` to disable this behavior. For detailed information about authentication configuration, see our [guide to customizing security and authentication](/docs/customize/authentication/configuration). ```yaml auth: OAuth2ClientCredentialsEnabled: true hoistGlobalSecurity: true ``` #### Disable security hoisting Set `hoistGlobalSecurity` to `false` to opt out: ```yaml auth: hoistGlobalSecurity: false ``` ### inferSSEOverload Enables the generation of method overloads for Python SDKs to provide better type safety for Server-Sent Events (SSE) operations. When set to `true` (default), Speakeasy will automatically create overloaded methods for operations that meet specific criteria: - The operation has a request body - The request body contains a `stream` field (boolean type) - The operation has exactly two responses: one `text/event-stream` and one `application/json` This feature is currently Python-specific, with support for other languages planned for future releases. ```yaml inferSSEOverload: true ``` ### baseServerUrl Used to declare the base server URL. It overrides the `servers` field in the OpenAPI document if present, or provides a server URL if the `servers` field is absent. ```yaml baseServerUrl: "speakeasy.bar/public/api/" ``` ### requestBodyFieldName Changes the field name of the request body parameter from using a flattened request object name to using a generic name. When set to `body`, the request body parameter will be named `body` instead of using the flattened request object name. This provides a more consistent and predictable parameter naming convention across your SDK methods. ```yaml requestBodyFieldName: body ``` ### mockServer Disables the generation and use of a mock HTTP server with generated tests. ```yaml mockServer: disabled: true ``` ### schemas Configuration for how OpenAPI schemas are processed during SDK generation. #### allOfMergeStrategy Controls how `allOf` constructs in OpenAPI schemas are merged. For detailed information about merge strategies, see the [allOf schemas documentation](/docs/customize/data-model/allof-schemas). - `deepMerge` (default for new SDKs): Recursively merges nested properties within objects, preserving properties from all schemas in the `allOf` array. - `shallowMerge` (legacy behavior): Replaces entire property blocks when merging, which can result in lost properties from earlier schemas. ```yaml schemas: allOfMergeStrategy: deepMerge ``` ### multipartArrayFormat Controls how arrays are serialized in multipart/form-data requests. This option determines whether array field names include brackets (`[]`) or use the RFC 7578-compliant approach of repeating the same field name. - `legacy` (default for existing SDKs): Appends `[]` to array field names (e.g., `files[]`). This maintains backward compatibility with existing SDKs but is non-compliant with RFC 7578. - `standard` (default for new SDKs): Uses the RFC 7578-compliant format by repeating the same field name for each array element (e.g., multiple `files` fields). This is the correct multipart/form-data encoding according to RFC 7578 Section 4.3. ```yaml multipartArrayFormat: standard ``` **Migration note**: Changing from `legacy` to `standard` is a breaking change. APIs expecting field names with `[]` suffixes will need to be updated to handle the RFC-compliant format. ### versioningStrategy Controls how SDK versions are determined during generation. For more information about SDK versioning, see our [SDK versioning documentation](/docs/sdks/manage/versioning). - `automatic` (default): Automatically bumps the SDK version based on changes to the OpenAPI spec, configuration, or generator version. - `manual`: Uses the version specified in `gen.yaml` as-is without automatic bumping. Use this when you want full control over SDK versioning. ```yaml versioningStrategy: manual ``` When set to `manual`, the SDK version will only change when you explicitly update the `version` field in your language-specific configuration or use `speakeasy bump` commands. # Go configuration options Source: https://speakeasy.com/docs/speakeasy-reference/generation/go-config import { Table } from "@/mdx/components"; This section details the available configuration options for the Go SDK. All configuration is managed in the `gen.yaml` file under the `go` section. ## Version and general configuration ```yml go: version: 1.2.3 modulePath: "github.com/my-company/company-go-sdk" sdkPackageName: "company" ```
## Additional dependencies ```yml go: additionalDependencies: axios: "0.21.0" ```
## Retractions You can use retractions to mark specific versions of your Go module as unsuitable for use. Each retraction requires a version and can optionally include a comment explaining why the version was retracted. ```yml go: retractions: - version: v1.0.0 comment: Published accidentally - version: v1.0.1 ```
## Method and parameter management ```yml go maxMethodParams: 4 methodArguments: "require-security-and-request" ```
## Security configuration ```yml go envVarPrefix: SPEAKEASY flattenGlobalSecurity: true ```
## Import management ```yml go imports: paths: callbacks: models/callbacks errors: models/errors operations: models/operations shared: models/components webhooks: models/webhooks ```
## Error and response handling ```yml go: responseFormat: "envelope-http" ```
## Nullable and optional field handling ```yml go: nullableOptionalWrapper: true ```
### When it applies The wrapper is generated only for fields that are: - Optional (the property is not listed in the parent schema's `required` array) - Nullable (the property `type` includes `"null"` in OpenAPI 3.1) For example, the following JSON Schema (OpenAPI 3.1) defines an optional, nullable `nickname`: ```yaml type: object required: - id properties: id: type: string nickname: type: - string - "null" ``` ### Generated code With `nullableOptionalWrapper: true`, the corresponding Go model uses a wrapper type: ```go type Pet struct { ID string `json:"id"` Nickname optionalnullable.OptionalNullable[string] `json:"nickname,omitempty"` } ``` Without the wrapper (when disabled for existing SDKs), the same field may be generated as a pointer type. ### Using the wrapper Set values using helper constructors on `OptionalNullable` and retrieve values via `Get()`, which returns `(*T, bool)` — `ok` indicates presence, and a `nil` pointer indicates an explicit null value: ```go // Set a present, non-null value pet := shared.Pet{} nickname := "Finn" pet.Nickname = optionalnullable.From(&nickname) // Read a value if val, ok := pet.Nickname.Get(); ok { if val == nil { fmt.Println("nickname is explicitly null") } else { fmt.Println("nickname:", *val) } } else { fmt.Println("nickname not set") } ``` Enabling this flag changes the generated field type and how values are set and read. This is a breaking change for existing SDKs and requires migrating code that accessed those fields directly or through pointer checks to use `optionalnullable.From(...)` and `.Get()`. # Java Configuration Options Source: https://speakeasy.com/docs/speakeasy-reference/generation/java-config import { Table } from "@/mdx/components"; This section details the available configuration options for the Java SDK. All configuration is managed in the `gen.yaml` file under the `java` section. ## Version and general configuration ```yml java: version: 1.2.3 projectName: "openapi" description: "A description of your SDK" ```
## Publishing ```yml java: groupID: "com.mycompany" artifactID: "my-sdk" githubURL: "https://github.com/mycompany/my-sdk" companyName: "My Company" companyURL: "https://www.mycompany.com" companyEmail: "support@mycompany.com" ```
## Base package name This package will be where the primary SDK class is located (and sub-packages will hold various types of associated generated classes): ```yaml java: packageName: com.mycompany.sdk ``` ## Additional Dependencies ```yml java: additionalDependencies: - "implementation:com.fasterxml.jackson.core:jackson-databind:2.12.3" - "testImplementation:junit:junit:4.13.2" ```
## License ```yml java: license: name: "The MIT License (MIT)" url: "https://mit-license.org/" shortName: "MIT" ```
## Method and parameter management ```yml java: maxMethodParams: 4 ```
## Security configuration ```yml java: flattenGlobalSecurity: true ```
## Import management ```yml java: imports: paths: callbacks: models/callbacks errors: models/errors operations: models/operations shared: models/components webhooks: models/webhooks ```
### Import paths
## Error and response handling ```yml java: clientServerStatusCodesAsErrors: false ```
# PHP Configuration Options Source: https://speakeasy.com/docs/speakeasy-reference/generation/php-config import { Table } from "@/mdx/components"; This section details the available configuration options for the PHP SDK. All configuration is managed in the `gen.yaml` file under the `php` section. ## Version and general configuration ```yml php: version: 1.2.3 packageName: "openapi/openapi" namespace: "OpenAPI\\OpenAPI" ```
## Method and parameter management ```yml php: maxMethodParams: 4 ```
## Security configuration ```yml php: flattenGlobalSecurity: true ```
## Import management ```yml php: imports: option: "openapi" paths: callbacks: models/Callbacks errors: models/Errors operations: models/Operations shared: models/Components webhooks: models/Webhooks ```
### Import paths
## Error and Response Handling ```yml php: clientServerStatusCodesAsErrors: true responseFormat: "flat" enumFormat: "enum" ```
## Laravel service provider When a PHP SDK is used within a Laravel application, Speakeasy is able to generate the needed [Service Provider](https://laravel.com/docs/master/providers) code to support seamless integration. > ...all of Laravel's core services, are bootstrapped via service providers. > > But, what do we mean by "bootstrapped"? In general, we mean registering things, including registering service container bindings, event listeners, middleware, and even routes. Service providers are the central place to configure the application. To enable the Laravel Service Provider generation, update the `gen.yaml` configuration setting `enabled` to `true`, and set `svcName` appropriately. ```yml php: laravelServiceProvider: enabled: true svcName: "openapi" ```
### Laravel service provider configuration
## Additional dependencies ```yml php: additionalDependencies: { "autoload": { "OpenAPI\\OpenAPI\\Lib\\": "lib/" }, "autoload-dev": { "OpenAPI\\OpenAPI\\Test\\": "Tests/" }, "require": { "firebase/php-jwt": "^6.10", "phpseclib/phpseclib": "^3.0" }, "require-dev": { "monolog/monolog": "^3.0" } } ```
### Additional dependencies configuration
# Python Configuration Options Source: https://speakeasy.com/docs/speakeasy-reference/generation/python-config import { Table, Callout } from "@/mdx/components"; This section details the available configuration options for the Python SDK. All configuration is managed in the `gen.yaml` file under the `python` section. ## Version and general configuration ```yml python: version: 1.2.3 authors: ["Author Name"] packageName: "openapi" moduleName: "openapi" packageManager: "uv" # or "poetry" to switch to poetry ```
## Description and URLs ```yml python: description: "Python Client SDK Generated by Speakeasy." homepage: "https://example.com" documentationUrl: "https://example.com/docs" ```
## Different package and module names You can configure a different name for the PyPI package and the module users will import from: ```yml python: packageName: "my-package" # Users will install with: pip install my-package moduleName: "my_module" # Users will import with: from my_module import SDK ``` This can be useful when you want the package name to follow PyPI conventions (using hyphens) but the module name to follow Python conventions (using underscores). ## Additional dependencies ```yml python: additionalDependencies: main: requests: "^2.25.1" dev: pytest: "^6.2.1" ```
## Method and parameter management ```yml python: maxMethodParams: 4 flatteningOrder: "parameters-first" methodArguments: "infer-optional-args" ```
## Security configuration ```yml python: envVarPrefix: "SPEAKEASY" flattenGlobalSecurity: true ```
## Import management ```yml python: imports: option: "openapi" paths: callbacks: "models/callbacks" errors: "models/errors" operations: "models/operations" shared: "models/shared" webhooks: "models/webhooks" ```
### Import paths
## Error and response handling ```yml python: clientServerStatusCodesAsErrors: true responseFormat: "flat" enumFormat: "enum" ```
## Async method configuration ```yml python: asyncMode: split # or "both" (default) ```
The `asyncMode` setting provides two patterns for handling async operations: **Method-based (`both`, default)**: Every operation has two methods - a synchronous version and an asynchronous version with an `_async` suffix. ```python sdk = MyAPI(api_key="...") # Synchronous operations result = sdk.list_users() # Asynchronous operations result = await sdk.list_users_async() ``` **Constructor-based (`split`)**: Separate constructors for synchronous and asynchronous clients. All method names are identical between sync and async versions. ```python # Synchronous client sync_sdk = MyAPI(api_key="...") result = sync_sdk.list_users() # Asynchronous client async_sdk = AsyncMyAPI(api_key="...") result = await async_sdk.list_users() ``` The constructor-based pattern eliminates method name duplication and provides clearer IDE suggestions. Switching to `asyncMode: split` is a breaking change. Existing SDK users will need to update their code to use the new constructor pattern. ## Server-sent events configuration ```yml python: sseFlatResponse: true ```
## Pytest configuration ```yml python: pytestFilterWarnings: - error - "ignore::DeprecationWarning" pytestTimeout: 300 ```
## Fix flags ```yml python: fixFlags: asyncPaginationSep2025: true ```
# Ruby Configuration Options Source: https://speakeasy.com/docs/speakeasy-reference/generation/ruby-config import { Table } from "@/mdx/components"; This section details the available configuration options for the Ruby SDK. All configuration is managed in the `gen.yaml` file under the `ruby` section. ## Version and general configuration ```yml ruby: version: 1.2.3 author: "Author Name" packageName: "custom-sdk" ```
## Method and parameter management ```yml ruby: maxMethodParams: 4 ```
## Module management ```yml ruby: module: "OpenApiSdk" ```
## Import management ```yml ruby: imports: option: "openapi" paths: callbacks: models/callbacks errors: models/errors operations: models/operations shared: models/components webhooks: models/webhooks ```
### Import paths
# Terraform configuration options Source: https://speakeasy.com/docs/speakeasy-reference/generation/terraform-config import { Table } from "@/mdx/components"; This section details the available configuration options for Terraform Provider generation. All configuration is managed in the `gen.yaml` file under the `terraform` section. ## Version and general configuration ```yml terraform: version: 1.2.3 author: "examplecorp" packageName: "examplecloud" ```
## Additions ```yml terraform: additionalDependencies: {} additionalResources: [] additionalDataSources: [] additionalEphemeralResources: [] ```
## Environment variables ```yml terraform: environmentVariables: - env: EXAMPLECLOUD_TOKEN providerAttribute: token ```
# Typescript Configuration Options Source: https://speakeasy.com/docs/speakeasy-reference/generation/ts-config import { Table, Callout } from "@/mdx/components"; This section details the available configuration options for the TypeScript SDK. All configuration is managed in the `gen.yaml` file under the `typescript` section. ## Version and general configuration ```yml typescript: version: 1.2.3 author: "Author Name" packageName: "custom-sdk" ```
## Additional JSON package ```yml typescript: additionalPackageJSON: license: "MIT" ```
## Additional dependencies ```yml typescript: additionalDependencies: dependencies: axios: "^0.21.0" devDependencies: typescript: "^4.0.0" peerDependencies: react: "^16.0.0" ```
## Package scripts and examples ```yml typescript: additionalScripts: format: "prettier --write src" docs: "typedoc --out docs src" custom-test: "vitest run --coverage" generateExamples: true compileCommand: ["npm", "run", "build"] usageSDKInit: "new Petstore({})" usageSDKInitImports: - package: "@petstore/sdk" import: "Petstore" type: "packageImport" ```
### How scripts are merged The feature uses an override strategy where additional scripts take precedence over default scripts: 1. **Default scripts** are generated automatically based on SDK configuration: ```json { "lint": "eslint --cache --max-warnings=0 src", "build": "tsc", "prepublishOnly": "npm run build" } ``` 2. **Test scripts** are added if tests are enabled: ```json { "test": "vitest run src --reporter=junit --outputFile=.speakeasy/reports/tests.xml --reporter=default", "check": "npm run test && npm run lint" } ``` 3. **Additional scripts** override defaults if they have the same name: ```yml typescript: additionalScripts: build: "custom-build-command" # Replaces default "tsc" build deploy: "npm publish" # Adds new script ``` 4. **Result** in `package.json`: ```json { "scripts": { "build": "custom-build-command", // Overridden "check": "npm run test && npm run lint", "deploy": "npm publish", // Added "lint": "eslint --cache --max-warnings=0 src", "prepublishOnly": "npm run build", "test": "vitest run src --reporter=junit --outputFile=.speakeasy/reports/tests.xml --reporter=default" } } ``` ## Method and parameter management ```yml typescript: maxMethodParams: 3 flatteningOrder: "parameters-first" methodArguments: "infer-optional-args" ```
## Security configuration ```yml typescript: envVarPrefix: SPEAKEASY flattenGlobalSecurity: true ```
## Module management ```yml typescript: moduleFormat: "dual" useIndexModules: true ```
For optimal bundle size and tree-shaking performance in modern applications, we recommend using `moduleFormat: "dual"` together with `useIndexModules: false`. This combination ensures maximum compatibility while enabling the best possible bundler optimizations. ## Import management ```yml typescript: imports: option: "openapi" paths: callbacks: models/callbacks errors: models/errors operations: models/operations shared: models/components webhooks: models/webhooks ```
### Import paths
## Error and response handling ```yml typescript: clientServerStatusCodesAsErrors: true responseFormat: "flat" enumFormat: "union" defaultErrorName: "SDKError" baseErrorName: "HTTPError" acceptHeaderEnum: false ```
## Model validation and serialization ```yml typescript: jsonpath: "rfc9535" zodVersion: "v4-mini" constFieldsAlwaysOptional: false modelPropertyCasing: "camel" unionStrategy: "populated-fields" laxMode: "lax" alwaysIncludeInboundAndOutbound: false exportZodModelNamespace: false ```
## Forward compatibility These options control how the SDK handles API evolution, allowing older SDK versions to continue working when APIs add new enum values, union types, or fields. ```yml typescript: forwardCompatibleEnumsByDefault: true forwardCompatibleUnionsByDefault: tagged-only ```
These options work together with `laxMode` and `unionStrategy` to provide robust forward compatibility. When all four features are enabled (the default for new TypeScript SDKs), your SDK will gracefully handle API evolution including new enum values, new union types, missing fields, and type mismatches. See the [forward compatibility guide](/docs/sdks/manage/forward-compatibility) for more details. ## Server-sent events configuration ```yml typescript: sseFlatResponse: false ```
## Advanced features ```yml typescript: enableReactQuery: false ```
# OpenAPI support matrix Source: https://speakeasy.com/docs/speakeasy-reference/supported/openapi import { Table } from "@/mdx/components"; The tables below provide an overview of the OpenAPI features supported by Speakeasy. ## ✅ Server configuration
## ⚠️ Path parameters serialization ([Path parameters](https://swagger.io/docs/specification/describing-parameters/#path-parameters))
## ⚠️ Query parameters serialization ([Query parameters](https://swagger.io/docs/specification/describing-parameters/#query-parameters) and [query](https://swagger.io/docs/specification/describing-parameters/#query))
## ✅ Request headers ([Header](https://swagger.io/docs/specification/serialization/#header))
## ⚠️ Request body serialization
## ⚠️ Response body deserialization
## ✅ Media-type patterns ([Media types](https://swagger.io/docs/specification/media-types)) ## ✅ Data types
### ✅ Union types
## ✅ Miscellaneous
\* Files needed for creating a fully compilable package that can be published to the relevant package manager without further changes. # Speakeasy Terraform provider support matrix Source: https://speakeasy.com/docs/speakeasy-reference/supported/terraform import { Table } from "@/mdx/components"; ## Provider components
## Supported OpenAPI semantics
### Terraform framework types from JSON Schema types
### JSON Schema subschema handling
# Speakeasy Workflow File Source: https://speakeasy.com/docs/speakeasy-reference/workflow-file import { Callout } from "@/mdx/components"; For most use cases we recommend interacting with the Speakeasy workflow file (`workflow.yaml`) through the `speakeasy configure` command. This command has subcommands to configure sources, targets, github setup and package publishing. All new targets created through `speakeasy quickstart` will automatically have a workflow file created in the `.speakeasy/` folder in the root of their target directory.
The workflow file is a file that dictates how the Speakeasy CLI will interact with sources and targets. The interaction is modelled as a workflow between sources and targets. A Source is one or more OpenAPI documents and Overlays merged together to create a single OpenAPI documents. A Target is a SDK, Terraform or other generated artifact from sources. # File Structure ## Speakeasy Version The version of the Speakeasy CLI to use to run the workflow. The `speakeasyVersion` field accepts three types of values: - **`latest`**: The Speakeasy CLI will perform a blue/green attempted upgrade to the latest version of the CLI. If there is a failure in generation, it will always re-run on the last successful version. This is the default and always tries to bring in generator upgrades (for example, to dependencies) where those changes compile, lint successfully, and pass tests successfully. - **`pinned`**: The Speakeasy CLI will always execute on the installed version and won't attempt to change versions. This can be useful for teams with alternative automation to control the Speakeasy CLI version (for example, using [mise](https://mise.jdx.dev/)). - **A specific version** (for example, `1.493.1`): The Speakeasy CLI will always run on this exact version. This can be useful for teams with stringent change management procedures. Available versions correspond to [Speakeasy CLI releases](https://github.com/speakeasy-api/speakeasy/releases). ```yaml workflowVersion: 1.0.0 speakeasyVersion: latest ``` ```yaml workflowVersion: 1.0.0 speakeasyVersion: pinned ``` ```yaml workflowVersion: 1.0.0 speakeasyVersion: 1.493.1 ``` ## Sources Sources can be added to a workflow programmatically `speakeasy configure sources` or manually by editing the workflow file. ### Sources Sources are the inputs to the workflow. A single Source is one or more OpenAPI documents and Overlays that are merged together to create a single OpenAPI document. ```yaml sources: my-source: inputs: - location: ./openapi.yaml - location: ./another-openapi.yaml # .... more openapi documents can be added here overlays: - location: ./overlay.yaml - location: ./another-overlay.yaml # .... more openapi overlays can be added here transformations: ``` ### SourceName Each Source is given a name. In this example the name is `my-source`. This name is used to reference the source in the workflow file. ```yaml my-source: ``` ### Inputs Each Source has a list of inputs. Each input is an OpenAPI document or Overlay. The OpenAPI documents and Overlays are merged together to create a single OpenAPI document. ```yaml inputs: - location: ./openapi.yaml - location: ./another-openapi.yaml # .... more openapi documents can be added here ``` ### Location Each input has a location. The location is the path to the OpenAPI document or Overlay. The path can be a local reference or a remote URL. If a URL is a used authentication may need to be provided. ```yaml - location: ./openapi.yaml - location: ./another-openapi.yaml # .... more openapi documents can be added here ``` ### Model namespace When merging multiple OpenAPI documents, schema components with the same name can cause naming conflicts in generated code. The `modelNamespace` option allows each input document to specify a namespace for its schema components, ensuring unique names in the merged output. ```yaml sources: merged-api: inputs: - location: service-a.yaml modelNamespace: serviceA - location: service-b.yaml modelNamespace: serviceB ``` When `modelNamespace` is specified, the merge process automatically adds `x-speakeasy-name-override` and `x-speakeasy-model-namespace` extensions to each schema component from that input. This preserves the original schema name for serialization while placing the generated model in a separate namespace to avoid conflicts. For example, if both `service-a.yaml` and `service-b.yaml` define a `Pet` schema: ```yaml # service-a.yaml components: schemas: Pet: type: object properties: name: type: string ``` ```yaml # service-b.yaml components: schemas: Pet: type: object properties: species: type: string ``` The merged output will contain both schemas with unique names and namespace extensions: ```yaml # merged.yaml components: schemas: serviceA_Pet: x-speakeasy-name-override: Pet x-speakeasy-model-namespace: serviceA type: object properties: name: type: string serviceB_Pet: x-speakeasy-name-override: Pet x-speakeasy-model-namespace: serviceB type: object properties: species: type: string ``` For more granular control over which schemas belong to which namespace, apply the `x-speakeasy-model-namespace` extension directly to individual schemas in the OpenAPI document instead of using `modelNamespace` in the workflow file. ### Transformations Sources can include transformations that modify the OpenAPI document before it's used to generate SDKs. Transformations are applied in order after merging inputs and applying overlays. ```yaml # .... more openapi overlays can be added here transformations: # Remove unused components from the OpenAPI document - removeUnused: true # Filter to include only specific operations - filterOperations: operations: getPets, createPet include: true # General cleanup of the OpenAPI document (formatting and style) ``` ### Output Each source can specify an output location where the merged OpenAPI document will be written. ```yaml output: ./merged-openapi.yaml ``` ### Registry Sources can be configured to publish to the API Registry found in the Speakeasy workspace. ```yaml registry: location: registry.speakeasy.com/my-org/my-api ``` ## Targets Targets can be added to a workflow programmatically `speakeasy configure targets` or manually by editing the workflow file. ### Targets Targets are the outputs of the workflow. A single Target is a SDK, Terraform or other generated artifact from sources. ```yaml targets: my-target: target: python source: my-source testing: enabled: true mockServer: enabled: false ``` ### TargetName Each Target is given a name. In this example the name is `my-target`. This name is used to reference the target in the workflow file. ```yaml my-target: ``` ### TargetType Each Target has a type. The target is the type of artifact that will be generated from the sources. The target can be one of the supported languages [here](/docs/sdks/create-client-sdks) ```yaml target: python ``` ### TargetSource Each Target has a source. The source is the name of the source that the target will be generated from. ```yaml source: my-source ``` ### Testing Each Target supports enabling testing as part of the workflow, if test generation is licensed. This will run target-specific testing, such as `go test` or `pytest`, after code generation. Use this with CLI-only `speakeasy run` development workflows (instead of separately calling `speakeasy test`) or GitHub Actions `mode: direct` or `mode: test` development workflows to ensure tests are successful with any potential code updates. ```yaml testing: enabled: true mockServer: enabled: false ``` ### MockServer Target testing, when licensed and enabled, starts a mock API server automatically as part of the workflow. This disables the mock API server, if the testing should always pointed at a test environment server URL instead. ```yaml mockServer: enabled: false ``` ### CodeSamples Each target can be configured to generate code samples and publish them to Speakeasy's registry. ```yaml codeSamples: output: codeSamples.yaml registry: location: registry.speakeasy.com/my-org/my-api/code-samples ``` # Generate MCP servers from OpenAPI documents Source: https://speakeasy.com/docs/standalone-mcp/build-server import { FileTree } from "nextra/components"; Learn how to build MCP servers from your OpenAPI documents using Speakeasy for production-ready servers with extensive customization options. ## Getting started with Speakeasy Use the following steps to build an MCP server using Speakeasy. - Install the Speakeasy CLI with the following commands: ```bash # Homebrew (macOS) brew install speakeasy-api/tap/speakeasy # Script Installation (macOS and Linux) curl -fsSL https://go.speakeasy.com/cli-install.sh | sh ``` - Run the Speakeasy quickstart command: ```bash speakeasy quickstart --mcp ``` - Indicate whether you plan to deploy your server on Cloudflare. [Cloudflare](https://developers.cloudflare.com/agents/guides/remote-mcp-server/) is a popular choice for hosting MCP servers. If selected, a Cloudflare Worker config file is generated with your MCP server code, making it easy to deploy your server to a Cloudflare Worker. These steps create a TypeScript-based MCP server in the specified directory, ready to be deployed into production. ## Example server code The generated MCP server includes a comprehensive file structure with TypeScript source code:
## Running your MCP server locally You can run your MCP server from the generated code, a published npm package, or the generated MCP Bundle (MCPB) package. ### Local development setup For local development and debugging, you can run your MCP server directly from the generated code: ```json { "mcpServers": { "MyAPI": { "command": "node", "args": ["/path/to/repo/bin/mcp-server.js", "start"], "env": { "API_TOKEN": "your-api-token-here" } } } } ``` This configuration allows you to do the following: - Debug your server code directly. - Make real-time changes during development. - Test API integrations locally. - Validate tool functionality before distribution. ### Testing with a published package Once you've published your MCP server to npm (following our [SDK publishing guide](/docs/publish-sdk)), you can test using the npm package format that will be used in production: ```json { "mcpServers": { "MyAPI": { "command": "npx", "args": ["your-npm-package@latest", "start"], "env": { "API_TOKEN": "your-api-token-here" } } } } ``` **Note:** You must first publish your package to npm before you can test it using this configuration. ### Using the generated MCPB For the most user-friendly testing experience, use the generated MCPB package. Claude Desktop and other MCP clients can load MCPB files directly: - **Locate your generated MCPB file** in the output directory (it typically has a `.mcpb` extension). - **Install the MCPB** in your MCP client following the client's installation process. - **Configure environment variables** if your server requires authentication. ## Advanced configuration You can further configure your MCP server to specify the tools or tool scopes it includes at runtime. ### Specify scopes at runtime When starting the MCP server, you can specify which scopes to include: ```json { "mcpServers": { "MyAPI": { "command": "npx", "args": ["your-npm-package@latest", "start", "--scope", "read"], "env": { "API_TOKEN": "your-api-token-here" } } } } ``` This example configuration only mounts tools tagged with the `read` scope, creating a read-only server. ### Specify individual tools You can further limit the subset of tools mounted on an MCP server by specifying individual tool names: ```json { "mcpServers": { "MyAPI": { "command": "npx", "args": ["your-npm-package@latest", "start", "--tool", "my_tool_1", "--tool", "my_tool_2"], "env": { "API_TOKEN": "your-api-token-here" } } } } ``` ## Distribution and hosting options Speakeasy generates MCP servers with three primary distribution methods. You can distribute your server as an npm package, a Cloudflare Worker, or an MCPB file. These methods aren't mutually exclusive — most customers use all three approaches to serve different use cases. ### Distribute your MCP server on npm Publishing your MCP server as an npm package is the most common starting point, as it provides the most flexible distribution method, in addition to the following benefits: - **Universal compatibility:** You can use a single command that works with all major MCP clients. - **Easy customization:** Users can modify tools, add tools, and extend functionality. - **Version management:** Standard npm versioning and dependency management apply. - **Team sharing:** Distribution across development teams is simple. ```json { "mcpServers": { "MyAPI": { "command": "npx", "args": ["your-npm-package@latest", "start"], "env": { "API_TOKEN": "your-api-token-here" } } } } ``` Follow the [SDK publishing guide](/docs/publish-sdk) for detailed instructions on publishing to npm and other package managers. ### Deploy your MCP server to Cloudflare Workers Cloudflare hosting enables OAuth authentication flows and eliminates the need for users to manage API keys directly. The benefits include: - **OAuth support:** End-users authenticate through OAuth instead of providing API keys. - **Serverless hosting:** No infrastructure management is required. - **Global distribution:** You can use Cloudflare's edge network for low latency. - **Same customization:** Full tool customization capabilities are available. Follow the [Cloudflare deployment guide](/docs/standalone-mcp/cloudflare-deployment) for detailed instructions on using Cloudflare hosting. ### Generate your MCP server as an MCPB file In addition to a drag-and-drop experience for end users (particularly in Claude Desktop), MCPB provides the following benefits: - **User-friendly installation:** Drag-and-drop installation in Claude Desktop. - **Guided setup:** Nicely prompts end-users for API keys and auth credentials. - **Self-contained format:** All dependencies and metadata bundled together. - **Cross-platform compatibility:** Works across different operating systems and MCP clients. Speakeasy automatically generates [MCP Bundle (`.mcpb` files)](https://github.com/anthropics/dxt) as part of MCP server generation. The MCPB includes: - **A `manifest.json` file**, containing metadata describing your MCP server - **All the necessary server files**, packaged for distribution - **Tool descriptions and parameters**, automatically inferred from your OpenAPI document - **Icon and branding assets**, if provided You can customize the generated MCPB manifest through the `gen.yaml` configuration file: ```yaml targets: mcp-typescript: mcpbManifestOverlay: icon: "https://example.com/my-icon.png" displayName: "My Custom API Tools" description: "Custom description for my MCP server" version: "1.0.0" ``` ### Hybrid distribution strategy You can use all three distribution methods simultaneously, giving users the flexibility to choose their preferred installation method based on their specific needs and technical requirements. ## Best practices When building MCP servers with Speakeasy: - **Use clear OpenAPI documentation:** Well-documented APIs generate better MCP tools. - **Implement proper error handling:** Ensure your API returns meaningful error messages. - **Use the `x-speakeasy-mcp` extension:** Customize tool names, descriptions, and scopes. - **Test thoroughly:** Verify that your MCP server works with different clients. - **Version your APIs:** Use semantic versioning for your OpenAPI documents. - **Monitor performance:** Track the usage and performance of your MCP tools. - **Provide clear branding:** Include a `.png` icon file in your project for automatic [MCPB](https://www.anthropic.com/engineering/desktop-extensions) icon detection. - **Customize MCPB metadata:** Use `mcpbManifestOverlay` in `gen.yaml` to provide custom display names and descriptions. ## Next steps Having set up your MCP server with Speakeasy, you can now learn to do the following: - **[Customize your MCP tools](/docs/standalone-mcp/customize-tools):** Customize your generated tools using the `x-speakeasy-mcp` OpenAPI extension. - **[Build custom resources](/docs/standalone-mcp/custom-resources):** Add custom tools and resources to your MCP server - **[Use custom prompts](/docs/standalone-mcp/custom-prompts):** Create reusable prompt templates. - **[Set up OAuth](/docs/standalone-mcp/setting-up-oauth):** Configure authentication for your MCP server. # Deploy to Cloudflare Workers Source: https://speakeasy.com/docs/standalone-mcp/cloudflare-deployment import { Callout } from "@/mdx/components"; Deploy your MCP server to Cloudflare Workers for global edge distribution and zero-ops scaling using Speakeasy's built-in Cloudflare deployment configuration. ## Prerequisites Before deploying to Cloudflare Workers, you need to do the following: - [Generate an MCP server](/docs/standalone-mcp/build-server) using Speakeasy. - Install the [Wrangler CLI](https://developers.cloudflare.com/workers/wrangler/install-and-update/). - Set up a Cloudflare account. ## Configuration Enable Cloudflare Worker deployment by adding the following configuration to your `.speakeasy/gen.yaml` file: ```yaml mcp-typescript: cloudflareEnabled: true cloudflareURL: https://your-worker-name.your-subdomain.workers.dev ``` ### Configuration options - **`cloudflareEnabled`**: Set to `true` to enable Cloudflare Worker deployment configuration. - **`cloudflareURL`**: This is the URL where your MCP server will be deployed. It should match the URL of your Cloudflare Worker. The `cloudflareURL` determines the endpoint where your MCP server will be accessible after deployment. Make sure this matches your intended Worker domain. ## Generating the deployment After configuring your `gen.yaml` file, regenerate your SDK to include Cloudflare Worker deployment files: ```bash speakeasy run ``` This creates an additional file in your generated SDK for the Cloudflare Worker. ## Deployment process Use the following steps to deploy your MCP server on a Cloudflare worker. - Authenticate with Cloudflare: ```bash npx wrangler login ``` - Navigate to your generated SDK directory and deploy your MCP server: ```bash npx wrangler deploy ``` - Verify that your MCP server is running at the URL specified in your `cloudflareURL` configuration by accessing it via curl: ```bash curl https://your-worker-name.your-subdomain.workers.dev/ ``` Alternatively, navigate to the URL in a browser window to view the MCP Server configuration. ## Using the deployed server Configure your MCP client to connect to the deployed Cloudflare Worker. Different clients use different configuration formats. ### Claude Code Use the Claude Code CLI to add your deployed MCP server with the following command: ```bash claude mcp add --transport sse MyAPI https://your-worker-name.your-subdomain.workers.dev/sse \ --header "X-API-Key: your-api-key-here" ``` ### Windsurf Add the server configuration to your Windsurf MCP settings: ```json { "mcpServers": { "MyAPI": { "serverUrl": "https://your-worker-name.your-subdomain.workers.dev/sse", "headers": { "X-API-Key": "your-api-key-here" } } } } ``` ### Cursor Configure your MCP server in Cursor's settings: ```json { "mcpServers": { "MyAPI": { "url": "https://your-worker-name.your-subdomain.workers.dev/mcp", "headers": { "X-API-Key": "your-api-key-here" } } } } ``` Note the different endpoint paths: Claude Code and Windsurf use `/sse` for server-sent events, while Cursor uses `/mcp` for the standard MCP protocol endpoint. ### Updating your deployment You can update your deployed MCP server as follows: - Make changes to your OpenAPI document. - Regenerate your SDK with the following command: ``` speakeasy run ``` - Redeploy by running the following command: ``` npx wrangler deploy ``` # Custom Prompts Source: https://speakeasy.com/docs/standalone-mcp/custom-prompts You can use [MCP prompts](https://modelcontextprotocol.io/docs/concepts/prompts) to create reusable prompt templates and workflows for MCP. Custom prompts allow you to define structured interactions that can be invoked by MCP clients. ## Building and registering custom MCP prompts Below are examples of custom MCP prompts that demonstrate different patterns for prompt creation: ```typescript filename="custom/customPrompts.ts" import { z } from "zod"; import { formatResult, PromptDefinition } from "../prompts.js"; const myNameArg = { first_name: z.string(), last_name: z.string() }; export const prompt$aboutMyName: PromptDefinition = { name: "tell-me-about-my-name-prompt", description: "tell me about the origins of my name", args: myNameArg, prompt: (_client, args, _extra) => ({ messages: [ { role: "user", content: { type: "text", text: `Please tell me about the origin of my name first_name: ${args.first_name} last_name: ${args.last_name}`, }, }, ], }), }; const prompt$aboutSpeakeasy: PromptDefinition = { name: "tell-me-about-speakeasy", prompt: (_client, _extra) => formatResult("Please tell me about the company Speakeasy"), }; ``` ```typescript filename="server.extensions.ts" import { prompt$aboutMyName, prompt$aboutSpeakeasy, } from "./custom/customPrompts.js"; import { Register } from "./extensions.js"; export function registerMCPExtensions(register: Register): void { register.prompt(prompt$aboutMyName); register.prompt(prompt$aboutSpeakeasy); } ``` ## Prompt patterns ### Simple prompts For basic prompts without parameters: ```typescript export const prompt$helpDesk: PromptDefinition = { name: "help-desk-assistant", description: "A helpful customer service assistant", prompt: (_client, _extra) => ({ messages: [ { role: "system", content: { type: "text", text: "You are a helpful customer service assistant. Be polite, professional, and solution-oriented.", }, }, ], }), }; ``` ### Parameterized prompts For prompts that accept arguments: ```typescript const codeReviewArgs = { language: z.string(), code: z.string(), focus: z.enum(["security", "performance", "style", "all"]).optional() }; export const prompt$codeReview: PromptDefinition = { name: "code-review-prompt", description: "Review code for best practices and issues", args: codeReviewArgs, prompt: (_client, args, _extra) => ({ messages: [ { role: "system", content: { type: "text", text: `You are an expert ${args.language} developer. Review the following code focusing on ${args.focus || "all aspects"}.`, }, }, { role: "user", content: { type: "text", text: `Please review this ${args.language} code:\n\n\`\`\`${args.language}\n${args.code}\n\`\`\``, }, }, ], }), }; ``` ### Multi-step prompts For complex workflows with multiple interactions: ```typescript const troubleshootArgs = { issue: z.string(), system: z.string(), urgency: z.enum(["low", "medium", "high"]) }; export const prompt$troubleshoot: PromptDefinition = { name: "troubleshooting-workflow", description: "Systematic troubleshooting workflow", args: troubleshootArgs, prompt: (_client, args, _extra) => ({ messages: [ { role: "system", content: { type: "text", text: `You are a technical support specialist. Follow a systematic approach to troubleshoot ${args.system} issues.`, }, }, { role: "user", content: { type: "text", text: `Issue: ${args.issue}\nSystem: ${args.system}\nUrgency: ${args.urgency}\n\nPlease provide step-by-step troubleshooting guidance.`, }, }, ], }), }; ``` ## Setting up MCP extensions To register your custom prompts, add them to your `server.extensions.ts` file: ```typescript filename="server.extensions.ts" import { Register } from "./extensions.js"; import { prompt$helpDesk, prompt$codeReview, prompt$troubleshoot } from "./custom/customPrompts.js"; export function registerMCPExtensions(register: Register): void { register.prompt(prompt$helpDesk); register.prompt(prompt$codeReview); register.prompt(prompt$troubleshoot); } ``` After adding the `server.extensions.ts` file and defining your custom prompts, execute `speakeasy run` to regenerate your MCP server with the new prompts. ## Best practices - **Use clear, descriptive names** for your prompts that indicate their purpose - **Provide detailed descriptions** to help users understand when to use each prompt - **Validate input parameters** using Zod schemas for type safety - **Structure messages logically** with appropriate roles (system, user, assistant) - **Handle edge cases** gracefully with fallback content - **Test prompts thoroughly** with various input combinations - **Keep prompts focused** on specific tasks rather than trying to handle everything - **Use consistent formatting** for similar types of prompts across your server # Custom Resources Source: https://speakeasy.com/docs/standalone-mcp/custom-resources An [MCP resource](https://modelcontextprotocol.io/docs/concepts/resources) represents any kind of data source that an MCP server can make available to clients. Each resource is identified by a unique URI and can contain either text or binary data. Resources can encompass a variety of things, including: - File contents (local or remote) - Database records - Screenshots and images - Static API responses ## Setting up MCP extensions To set up MCP extensions, create a new file under the `mcp-server` directory and name it `server.extensions.ts`. The file should expose the following function contract exactly: ```typescript filename="server.extensions.ts" import { Register } from "./extensions.js"; export function registerMCPExtensions(register: Register): void { register.tool(...); register.resource(...); register.prompt(...); } ``` This function can be used to register custom tools, resources, and prompts on a generated MCP server. After adding the `server.extensions.ts` file and defining the custom tools and resources, execute `speakeasy run`. ## Building and registering custom MCP resources Below is an example of a custom MCP resource that embeds a local PDF file as a resource in an MCP server. The custom resource must fit the `ResourceDefinition` or `ResourceTemplateDefinition` type exposed by Speakeasy and define a `read` function for reading data from the defined URI. Speakeasy exposes a `formatResult` utility function from `resources.ts` that you can use to ensure the result is returned in the proper MCP format. Using this function is optional, as long as the return matches the required type. ```typescript filename="custom/aboutSpeakeasyResource.ts" import fs from "node:fs/promises"; import { formatResult, ResourceDefinition } from "../resources.js"; export const resource$aboutSpeakeasy: ResourceDefinition = { name: "About Speakeasy", description: "Reads the about Speakeasy PDF", resource: "file:///Users/ryanalbert/about_speakeasy.pdf", scopes: [], read: async (_client, uri, _extra) => { try { const pdfContent = await fs.readFile(uri, null); return formatResult(pdfContent, uri, { mimeType: "application/pdf", }); } catch (err) { console.error(err); return { contents: [ { isError: true, uri: uri.toString(), mimeType: "application/json", text: `Failed to read PDF file: ${String(err)}`, }, ], }; } }, }; ``` ```typescript filename="server.extensions.ts" import { resource$aboutSpeakeasy } from "./custom/aboutSpeakeasyResource.js"; import { Register } from "./extensions.js"; export function registerMCPExtensions(register: Register): void { register.resource(resource$aboutSpeakeasy); } ``` ## Resource types and patterns ### Static resources For resources that don't change based on parameters, use static resource definitions: ```typescript export const resource$staticConfig: ResourceDefinition = { name: "Application Config", description: "Static application configuration", resource: "config://app/settings", scopes: [], read: async (_client, uri, _extra) => { const config = { version: "1.0.0", environment: "production", features: ["auth", "analytics"] }; return formatResult(JSON.stringify(config), uri, { mimeType: "application/json", }); }, }; ``` ### Dynamic resources For resources that change based on parameters, use resource templates: ```typescript export const resource$userProfile: ResourceTemplateDefinition = { uriTemplate: "user://{userId}/profile", name: "User Profile", description: "User profile information", mimeType: "application/json", read: async (_client, uri, _extra) => { const userId = extractUserIdFromUri(uri); const profile = await fetchUserProfile(userId); return formatResult(JSON.stringify(profile), uri, { mimeType: "application/json", }); }, }; ``` ## Best practices - Use clear, descriptive URI schemes like `file://`, `config://`, or `user://` - Keep resource data consistent and read-only - Validate inputs or file paths to prevent injections or errors - Set correct MIME types so clients can parse content properly - Handle errors gracefully and return meaningful error messages - Use the `formatResult` utility for consistent response formatting # Customize Tools Source: https://speakeasy.com/docs/standalone-mcp/customize-tools You can customize how your API operations are exposed as MCP tools using the `x-speakeasy-mcp` OpenAPI extension. This allows you to control tool names, descriptions, scopes, and whether specific operations should be included in your MCP server. ## Set the configuration options The `x-speakeasy-mcp` extension can be used on any operation to customize the MCP tool: ```yaml filename="openapi.yaml" paths: /products: post: operationId: createProduct tags: [products] summary: Create product description: API endpoint for creating a product in the CMS x-speakeasy-mcp: disabled: false name: create-product scopes: [write, ecommerce] description: | Creates a new product using the provided form. The product name should not contain any special characters or harmful words. # ... ``` ### `disabled` (_optional_, default: `false`) If set to `true`, the generator will not create the MCP tool for this operation. ### `name` (_optional_) This is the name of the MCP tool. The default value is derived from `operationId`, `tags`, `x-speakeasy-name-override`, and `x-speakeasy-name-group`. In the example above, the default name would be `products_create-product`. ### `title` (_optional_) A human-friendly display name for the tool. While `name` serves as the programmatic identifier, `title` provides a readable name shown in client interfaces like VS Code Copilot Chat. For example, a tool with `name: "search_repositories"` could have `title: "Search repositories"` for better user experience. ```yaml x-speakeasy-mcp: name: search_repositories title: Search repositories description: Search for GitHub repositories using various filters ``` ### `scopes` (_optional_) You can use scopes to tag tools so that users can choose which set of tools they want to mount when the MCP server starts. For example, tagging relevant operations with a `read` scope allows users to start a server in read-only mode. ### `description` (_optional_) Each MCP tool description is passed as context to MCP clients and language models. The default value is the OpenAPI operation summary and description. It's a good practice to review and customize these descriptions for better context. ### `readOnlyHint` (_optional_, default: `false`) Indicates whether the tool modifies its environment. Set to `true` for tools that only read data without making changes. ```yaml x-speakeasy-mcp: readOnlyHint: true description: Retrieve driver statistics without modifying any data ``` ### `destructiveHint` (_optional_, default: `true`) Indicates whether the tool performs destructive updates to its environment. Set to `false` for tools that only perform additive updates. This property is meaningful only when `readOnlyHint` is `false`. ```yaml x-speakeasy-mcp: readOnlyHint: false destructiveHint: true description: Delete a driver record permanently from the system ``` ### `idempotentHint` (_optional_, default: `false`) Indicates whether calling the tool repeatedly with the same arguments has no additional effect on its environment. Set to `true` for idempotent operations. This property is meaningful only when `readOnlyHint` is `false`. ```yaml x-speakeasy-mcp: readOnlyHint: false idempotentHint: true description: Update driver status - calling multiple times with same status has no additional effect ``` ### `openWorldHint` (_optional_, default: `true`) Indicates whether the tool interacts with an "open world" of external entities. Set to `false` for tools whose domain of interaction is closed. For example, a web search tool has an open world, whereas a memory tool does not. ```yaml x-speakeasy-mcp: openWorldHint: false description: Query the internal driver database - does not access external systems ``` ## Use overlays [Overlays](/openapi/overlays) are a convenient way you can add the `x-speakeasy-mcp` extension to existing OpenAPI documents without modifying them. To create an Overlay file, you can use the Speakeasy [Overlay Playground](https://overlay.speakeasy.com/). For example, you can add scopes based on HTTP methods: ```yaml filename="overlay.yaml" overlay: 1.0.0 info: title: Add MCP scopes version: 0.0.0 actions: - target: $.paths.*["get","head","query"] update: { "x-speakeasy-mcp": { "scopes": ["read"] } } - target: $.paths.*["post","put","delete","patch"] update: { "x-speakeasy-mcp": { "scopes": ["write"] } } ``` ## Advanced usage ### Specify scopes at runtime When starting the MCP server, you can specify which scopes to include using the `--scope` flag: ```json { "mcpServers": { "MyAPI": { "command": "npx", "args": ["your-npm-package@latest", "start", "--scope", "read"], "env": { "API_TOKEN": "your-api-token-here" } } } } ``` This example configuration only mounts tools tagged with the `read` scope, creating a read-only server. You can specify multiple scopes by repeating the flag: ```json { "mcpServers": { "MyAPI": { "command": "npx", "args": ["your-npm-package@latest", "start", "--scope", "read", "--scope", "admin"], "env": { "API_TOKEN": "your-api-token-here" } } } } ``` ### Specify individual tools You can further limit the subset of tools mounted on an MCP server by specifying individual tool names using the `--tool` flag: ```json { "mcpServers": { "MyAPI": { "command": "npx", "args": ["your-npm-package@latest", "start", "--tool", "list-products", "--tool", "get-product"], "env": { "API_TOKEN": "your-api-token-here" } } } } ``` # Standalone MCP Server Generation Source: https://speakeasy.com/docs/standalone-mcp/introduction import { Callout } from "@/mdx/components"; Model Context Protocol (MCP) servers enable AI agents to interact with your APIs in a natural, conversational way. With Speakeasy, you can generate production-ready MCP servers directly from your OpenAPI/Swagger specification, without needing to write any code. ## What's New? Previously, MCP servers were generated as part of TypeScript SDK generation. Now, you can generate standalone MCP servers independently: - **Direct Generation**: Generate MCP servers directly from your OpenAPI spec - **Language Agnostic**: Works with any API, regardless of the implementation language - **Cloudflare Ready**: Optional Cloudflare Worker configuration for easy deployment - **Desktop Extension Support**: Automatic generation of Anthropic Desktop Extensions (.dxt) ## Key Features - **Automatic Tool Generation**: Each API operation becomes a discoverable tool - **Type Safety**: Uses OpenAPI/Swagger types for accurate request/response handling - **Customization Options**: Control tool names, descriptions, and more via OpenAPI extensions - **Scoping Support**: Tag and control which operations are available to AI agents - **Production Ready**: Built for reliability and performance at scale ## Getting Started Follow our [step-by-step guide](/docs/standalone-mcp/build-server) to generate your first MCP server, or explore our guides on: - [Customizing your MCP Server](/docs/standalone-mcp/customize-tools) - [Deploying to Cloudflare Workers](/docs/standalone-mcp/cloudflare-deployment) Want to understand MCP better? Check out our [introduction to MCP](/mcp/overview) and [AI agents overview](/mcp/using-mcp/ai-agents/introduction). # Adapting Speakeasy MCP Servers for Remote Deployment Source: https://speakeasy.com/docs/standalone-mcp/remote-mcp-servers import { Callout } from "@/mdx/components"; Speakeasy-generated MCP servers are designed to run locally using Node.js, but they can be adapted into remote services using MCP's [Streamable HTTP transport specification](https://modelcontextprotocol.io/specification/2025-06-18/basic/transports#streamable-http). This adaptation unlocks several key advantages: - **Simplified deployment**: Users don't need Node.js, npm, or other local tooling installed - **Flexible hosting**: Deploy to any cloud provider, container platform, or server infrastructure - **Distribution independence**: Eliminate reliance on npm registries for server distribution - **Multi-client support**: Handle concurrent connections from multiple MCP clients This guide demonstrates how to prepare a Speakeasy-generated MCP server for cloud deployment by wrapping it in an HTTP framework like Express.js, enabling it to serve multiple clients simultaneously over standard HTTP connections. ## Prerequisites Before starting, readers will need the following: - [Node.js and npm installed](https://nodejs.org/en/download/) - [A Speakeasy-generated MCP Server](/docs/standalone-mcp/build-server) ## Testing the MCP Server Locally While working on the MCP server, take advantage of the [MCP Inspector](https://modelcontextprotocol.io/legacy/tools/inspector) to test your work locally. First, run the inspector with the following command: ```bash npx @modelcontextprotocol/inspector ``` Once the inspector is running, set the transport to "Streamable HTTP" and the URL to the address of the local MCP endpoint, which is `http://localhost:3000/mcp` in this tutorial. This will allow you to connect to the MCP server and verify it's working correctly before deployment. ![MCP Inspector with Streamable HTTP transport](/assets/docs/mcp-inspector_streamable-http-local-config.png) ## Preparing the HTTP Server From a terminal, navigate to the root directory of your Speakeasy-generated MCP server project, and create a new file for the HTTP server: ```bash touch src/http-server.ts ``` In this file, we will set up an Express.js server to handle incoming HTTP requests and route them to the MCP server. First, we'll set up the skeleton for the HTTP server - a thin wrapper around the generated MCP server. For now, it only requires a single endpoint to handle incoming requests. ```typescript import express from 'express'; const app = express(); app.use(express.json()); app.post('/mcp', async (req, res) => { // Here, we will handle incoming MCP requests }); // This GET endpoint is required by the protocol for opening SSE connections app.get('/mcp', async (req, res) => { res.status(405).send('Method not allowed: SSE not implemented.'); }) // Start the server and listen for requests app.listen(3000, () => { console.log('Server listening on port 3000'); }); ``` The MCP protocol requires a `GET /mcp` endpoint for establishing Server-Sent Events (SSE) connections. In this example, we return a `405 Method Not Allowed` response for this endpoint, as we are not implementing SSE support for the scope of this guide. Read the [MCP Specification](https://modelcontextprotocol.io/specification/2025-06-18/basic/transports#listening-for-messages-from-the-server) for more details. Start the server with the following command: ```bash # using the project's installation of Bun npm exec bun -- --watch src/http-server.ts # or - if you have Bun installed globally bun --watch src/http-server.ts ``` This will start the server and listen for incoming requests on port `3000`. Trying to connect to this server with the MCP Inspector will result in an error, so let's implement the MCP transport handling logic next. ## Handling MCP Client Requests To handle requests from an MCP client (eg: Claude, Cursor, MCP Inspector, etc), we'll need to implement the MCP transport logic in the `POST /mcp` endpoint. We'll accomplish this by constructing an internal MCP server instance, and connecting it to a transport that can handle incoming HTTP requests. ```typescript import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js"; import express from "express"; import { createConsoleLogger } from "./mcp-server/console-logger.js"; import { createMCPServer } from "./mcp-server/server.js"; const app = express(); app.use(express.json()); app.post("/mcp", async (req, res) => { // Key Component #1: Create the MCP server instance const mcpServer = createMCPServer({ logger: createConsoleLogger("warning"), // or info/debug/error }); // Key Component #2: Create the MCP transport instance const mcpTransport = new StreamableHTTPServerTransport({ sessionIdGenerator: undefined, // Stateless - no session tracking }); // Key Component #3: Connect the MCP server to the transport await mcpServer.connect(mcpTransport); // Key Component #4: Handle the request using the MCP transport await mcpTransport.handleRequest(req, res, req.body); }); app.get('/mcp', async (req, res) => { res.status(405).send('Method not allowed: SSE not implemented.'); }) // Start the server and listen for requests app.listen(3000, () => { console.log("MCP server listening on port 3000"); }); ``` Let's break down the **key components** of this code: - **Key Component #1: Create the MCP Server Instance** - The MCP Server instance manages the server's capabilities and handles capability negotiation with clients. It processes resource discovery, tool registration, and routes MCP protocol messages. - **Key Component #2: Create the MCP Transport Instance** - The Transport handles the communication details between HTTP requests and MCP protocol messages. This includes aspects such as headers, status codes, encoding, and streaming. Setting `sessionIdGenerator` to `undefined` makes the implementation stateless - each request is independent with no session tracking. - **Key Component #3: Connect the MCP Server to the Transport** - This connects the MCP server to the transport, establishing the bridge between MCP protocol logic and HTTP communication. - **Key Component #4: Handle the Request Using the MCP Transport** - This processes the incoming HTTP request through the MCP transport layer and sends the appropriate MCP response back to the client. After implementing the above code, the MCP Inspector (and other clients) should be able to connect to the server, list its capabilities (tools, prompts, resources), and even execute *unauthenticated* tools. ![MCP Inspector with Streamable HTTP transport](/assets/docs/mcp-inspector_streamable-http-local-tool-list.png) If your MCP server needs to be able to use tools that interact with authenticated API endpoints, keep reading to learn how to accept an authentication header from the client, and use it to authenticate requests to the API. ## Adding Authentication Support Our HTTP server can also accept headers from a client and pass them to the underlying MCP server. This is useful for scenarios where the MCP server needs to authenticate requests to an API. The following example accepts an `Authorization` header from the client, and uses it to authorize the underlying API client, which is used by the MCP server to make requests to the API. ```typescript import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js"; import express from "express"; import { MySdkCore } from "./core.js"; import { createConsoleLogger } from "./mcp-server/console-logger.js"; import { createMCPServer } from "./mcp-server/server.js"; const app = express(); app.use(express.json()); app.post("/mcp", async (req, res) => { const apiKey = req.headers.authorization; // Key Component #1: Initialize the generated API client with the optional // credential const apiClient = new MySdkCore({ security: apiKey ? { api_key: apiKey } : undefined, }); const mcpServer = createMCPServer({ logger: createConsoleLogger("warning"), // Key Component #2: Pass the API client to the MCP server getSDK: () => apiClient, }); const mcpTransport = new StreamableHTTPServerTransport({ sessionIdGenerator: undefined, }); await mcpServer.connect(mcpTransport); await mcpTransport.handleRequest(req, res, req.body); }); app.get('/mcp', async (req, res) => { res.status(405).send('Method not allowed: SSE not implemented.'); }) // Start the server and listen for requests app.listen(3000, () => { console.log("MCP server listening on port 3000"); }); ``` Let's review the key components again: - **Key Component #1: Initialize the API Client with the Optional Credential** - This initializes the API client with the `Authorization` header from the request, if it exists. This allows the MCP server to authenticate requests to the API. - **Key Component #2: Pass the API Client to the MCP Server** - This passes the authenticated API client to the server, so calls can be made to secured API operations. Now, when the MCP Inspector (or any other client) connects to the server, it can include an `Authorization` header with an API key or token. The MCP server will use this header to authenticate requests to the API client, allowing it to access secured operations and resources. This pattern can be used further configure the API client. For example, an `X-Env` header could be sent to the server from the client, which can be used to select the `serverUrl` to use for the API client (eg: `production` or `sandbox`): ```typescript const serverUrl = req.headers["x-env"] === "sandbox" ? "https://api.sandbox.acme.io" : "https://api.acme.io"; const apiClient = new MySdkCore({ security: apiKey ? { api_key: apiKey } : undefined, serverUrl: serverUrl, }); ``` # Add OAuth to an MCP server Source: https://speakeasy.com/docs/standalone-mcp/setting-up-oauth import { Callout } from "@/mdx/components"; This guide demonstrates how to configure authentication for MCP servers using different OAuth methods. For assistance with OAuth implementation, including proxy setup and DCR compliance, [reach out to Gram Support](https://calendly.com/sagar-speakeasy/30min) for white-glove service. From March 2025, the MCP specification began recommending OAuth-based authentication. However, most existing OAuth implementations don't meet [MCP requirements](https://modelcontextprotocol.io/specification/draft/basic/authorization#protocol-requirements). The specification calls for OAuth 2.1 and support for Dynamic Client Registration (DCR), which most major OAuth providers — including Google, GitHub, and Microsoft Azure AD — don't support. This mismatch between MCP's vision and existing OAuth infrastructure creates a common implementation barrier for enterprise adoption. Gram bridges this gap by supporting multiple authentication approaches, from simple token-based methods to complex OAuth proxy solutions. How you set up authentication depends on the OAuth capabilities of your underlying API and the intended purpose of your MCP server. Understanding these different approaches and their trade-offs is essential for selecting the right authentication strategy. ## Using the Authorization Code Flow The Authorization Code Flow enables user-interactive OAuth with proper consent screens. However, it requires OAuth providers to support specific MCP requirements. ### With DCR If you want to host an MCP server for large-scale use by external developers, you should plan to build out support for DCR. For example,[Stripe](https://docs.stripe.com/mcp) and [Asana](https://developers.asana.com/docs/integrating-with-asanas-mcp-server) have both added support for DCR to their APIs to accommodate MCP. If the API is already configured to support DCR, enabling the Authorization Code Flow on Gram is simple: - Create a manifest file for the OAuth server in Gram. - Attach the manifest to your toolset. ### Without DCR Because most APIs don't support DCR, Gram offers an OAuth proxy that translates between MCP requirements and standard OAuth implementations. The proxy uses a specific client ID and secret to access the API on behalf of the MCP server end users. This is useful for MCP servers that won't be exposed to the public, or in cases where a server acting as a single `client_id` is acceptable. For example, the [Cloudflare OAuth proxy](https://blog.cloudflare.com/remote-model-context-protocol-servers-mcp/#workers-oauth-provider-an-oauth-2-1-provider-library-for-cloudflare-workers) doesn't support DCR. The OAuth proxy works using the following mechanisms: - **Proxy registration:** The proxy exposes DCR-compliant endpoints to MCP clients. - **Token translation:** It converts proxy tokens to a set of real provider tokens. - **Flow management:** It handles the OAuth dance between the client and the actual provider. - **State storage:** It maintains token mappings and authorization state. If you want to implement an OAuth proxy in Gram, please [book time with our team](https://calendly.com/sagar-speakeasy/30min). We'll get you up and running. ## Using the Client Credentials grant The Client Credentials grant is a simpler authentication method. The server exchanges a client ID and secret for access tokens. Gram handles the token exchange process automatically. Implement the Client Credentials grant in Gram as follows: - Upload the OpenAPI document to Gram - Navigate to the **Environments** tab - Add the `CLIENT_ID` (application client identifier) and `CLIENT_SECRET` (application client secret) environment variables. - Attach the environment to your toolset. ## Using access token authentication Access token authentication allows passing pre-obtained tokens directly to the MCP server. This method works with any OAuth provider, regardless of DCR support. Implement access token authentication in Gram as follows: - Obtain the access token from your OAuth provider. - Navigate to the **Environments** tab. - Add the `ACCESS_TOKEN` environment variable. - Attach the environment to your toolset. Popular services like GitHub use this approach. While technically OAuth-based, no OAuth flow occurs through the MCP client. # Generate a Terraform provider from an OpenAPI document Source: https://speakeasy.com/docs/terraform/create-terraform import { Callout, Table } from "@/mdx/components"; Terraform is an infrastructure-as-code tool that uses providers to manage cloud infrastructure through API calls. Creating and maintaining Terraform providers, which are typically written in Go, requires specialized skills and frequent updates to keep up with API changes. Speakeasy simplifies creating and maintaining Terraform providers by generating providers from OpenAPI documents. This eliminates the need for Go expertise, keeps providers up-to-date, and reduces the complexity of developing and maintaining providers for cloud environments. For a detailed overview of supported features, refer to the [Terraform support matrix](/docs/speakeasy-reference/supported/terraform). ## Prerequisites Creating a Terraform provider with Speakeasy requires: - The [Speakeasy CLI](/docs/speakeasy-reference/cli/getting-started) - An API spec in a supported format:
If you are using an unsupported spec format, use these tools to help you convert to a supported format: - [Swagger2.0 -> OpenAPI v3.0](https://github.com/swagger-api/swagger-converter) - [Postman -> OpenAPI v3.0](https://kevinswiber.github.io/postman2openapi/) ## Add annotations Use the `x-speakeasy-entity` annotation to specify objects to be included as Terraform entities in the provider. ```yaml paths: /pet: post: ... x-speakeasy-entity-operation: Pet#create ... Pet: x-speakeasy-entity: Pet ... ``` Terraform usage: ```HCL resource "petstore_pet" "myPet" { ... } ``` Speakeasy infers Terraform types from the JSON Schema, focusing on the semantics of the `CREATE` and `UPDATE` requests and responses. No specific Terraform types need to be defined in the OpenAPI document. 1. **Required vs optional:** If a property is required in the `CREATE` request body, it's marked as `Required: true`; otherwise, it's `Optional: true`. 2. **Computed properties:** Properties that appear in a response body but are absent from the `CREATE` request are marked as `Computed: true`. This indicates that Terraform will compute the properties' values. 3. **The `ForceNew` property:** If a property exists in the `CREATE` request but is not present in the `UPDATE` request, it's labeled `ForceNew`. 4. **Enum validation:** When an attribute is defined as an enum, Speakeasy configures a `Validator` for runtime type checks. This ensures that all request properties precisely match one of the enumerated values. 5. **`READ`, `UPDATE`, and `DELETE` dependencies**: Every parameter essential for `READ`, `UPDATE`, or `DELETE` operations must either be part of the `CREATE` API response body or be consistently required in the `CREATE` API request. This ensures that all necessary parameters are available for these operations. Use additional `x-speakeasy` [annotations](/docs/terraform) to customize the provider as necessary. ## Enhance generated documentation Speakeasy helps you autogenerate documentation using the HashiCorp `terraform-plugin-docs` tools and packages. For best results, we recommend that you: 1. **Include descriptions:** Ensure the OpenAPI document contains detailed descriptions of resources, attributes, and operations. Clear and concise descriptions help understand the purpose and use of each component. 2. **Provide examples:** Use examples in the OpenAPI document to illustrate how resources and attributes should be configured. Speakeasy leverages these examples to generate usage snippets for reference when starting with the provider. The Swagger Petstore generates a usage snippet for the pet resource similar to the following: ```go "petstore_pet" "my_pet" { id = 10 name = "doggie" photo_urls = [ "...", ] }. ``` ## Generate a Terraform provider Run the Speakeasy `quickstart` command: ```bash speakeasy quickstart ``` Follow the interactive guide, providing the necessary information when prompted, including the path to the spec. Select `terraform` as the language. After completing the quickstart, regenerate the Terraform provider at any point by running `speakeasy run`. ## Guidance on modeling entities ### Repository naming Name the provider and GitHub repository `terraform-provider-XXX`, where `XXX` becomes the short name of the provider, also known as the **provider type name**. The provider type name should preferably be `[a-z][a-z0-9]`, although hyphens and underscores are also valid and can be included in the name if necessary. ### Entity naming When naming entities that you want Speakeasy to convert to Terraform resources, use PascalCase to ensure the names are translated to Terraform's underscore naming. For list endpoints, pluralize the PascalCase name. After completing the quickstart, regenerate the Terraform provider at any time by running `speakeasy run`. ### Modeling entities First, find the list operation for an API entity or resource. Usually, it is a `GET` on `/something`. Annotate the list operation with `x-speakeasy-entity-operation: XXX#read`. Now, find the CREATE, READ, UPDATE, and DELETE (CRUD) operations for an API resource. Usually, these take the form of a `POST` on `/something` and operations on `/something/{id}`. Annotate the CRUD operations with `x-speakeasy-entity-operation: XXX#create`. Ensure the CREATE response returns data. Some API frameworks don't output it, even though they generally have to return data such as an identifier for the resource. Finally, check whether the `GET` (not list) read response includes an extra data property or similar element between the root of the response schema and the actual data. If the `GET` read response does have an additional data property, add the `x-speakeasy-entity: XXX` annotation to the object beneath that data property (not on the data itself). Most APIs use a shared `component`, which is often the best place for entity annotation. ## Frequently asked questions **Do the generated Terraform providers support importing resources?** Yes, generated Terraform providers support importing resources. However, certain prerequisites and considerations must be taken into account: **Prerequisites** 1. **API specification:** Ensure the OpenAPI document defines an annotated and type-complete API operation for reading each resource. Tag the operation with `x-speakeasy-entity-operation: MyEntity#read`. 2. **Complete `READ` operation:** Attributes of a resource not defined in the `READ` API are set to `null` by Terraform during the import process. **Simple keys** A simple key is a single required ID field that is directly exposed to `terraform import` operations. For example, if the `pet` resource has a single `id` field, the import command will look like this: `terraform import petstore_pet.my_pet my_pet_id`. **Handling composite keys** Speakeasy natively supports the direct import of resources with multiple ID fields. Speakeasy generates code that enables import functionality by requiring users to provide a JSON-encoded object with all necessary parameters. In addition to generating documentation, Speakeasy generates appropriate error messages to be displayed if the proper syntax is not followed. **Import composite keys by block** An import block allows you to import a resource into the Terraform state by generating the corresponding Terraform configuration. Using a composite key, the import block will look like this: ```hcl filename="test.tf" import { id = jsonencode({ primary_key_one: "9cedad30-2a8a-40f7-9d65-4fabb04e54ff" primary_key_two: "e20c40a0-40e8-49ac-b5d0-6e2f41f9e66f" }) to = my_test_resource.my_example } ``` ```bash terraform plan -generate-config-out=generated.tf ``` **Import composite keys using the CLI** To import a resource with composite keys using the Terraform CLI, use the `terraform import` command: ```bash terraform import my_test_resource.my_example '{ "primary_key_one": "9cedad30-2a8a-40f7-9d65-4fabb04e54ff", "primary_key_two": "e20c40a0-40e8-49ac-b5d0-6e2f41f9e66f" }' ``` # Advanced features Source: https://speakeasy.com/docs/terraform/customize/advanced-features ## Speciality annotations The annotations in this section are not commonly used within Speakeasy. We recommend contacting our team to help you determine whether they are correct for you. ### Force-marking a property as read-only The `x-speakeasy-param-readonly` extension marks a property as read-only. Any user attempt to modify it in Terraform will result in a runtime error. This prevents unintended changes to critical properties in Terraform configurations. ```yaml components: schemas: Pet: type: object properties: name: type: string id: type: integer x-speakeasy-param-readonly: true ``` ### Force-designating a property as optional Apply `x-speakeasy-param-optional` to any property to designate it as optional. This extension takes precedence over the required attribute in the JSON Schema specification, providing flexibility in Terraform configurations by allowing optional settings for certain properties. ```yaml components: schemas: Pet: type: object properties: name: type: string id: type: integer x-speakeasy-param-optional: true ``` ### Forcing resource recreation on property change Properties marked with `x-speakeasy-param-force-new` will cause the associated Terraform resource to be destroyed and recreated whenever the property value changes. This ensures that any alteration to the property triggers a complete recreation of the object. ```yaml components: schemas: Pet: type: object properties: name: type: string id: type: integer x-speakeasy-param-force-new: true ``` ### Updating behavior for plan-only attributes The `x-speakeasy-terraform-plan-only` extension ensures that only the values from the Terraform plan are used during updates, overriding any prior state or default values provided by the API. By preventing prior state values from being merged into the update request, the annotation ensures that omitted or null values in the plan are correctly reflected in API calls. ```yaml components: schemas: Pet: type: object properties: properties: name: type: string id: type: integer nullable: true x-speakeasy-terraform-plan-only: true ``` ## Deduplicating Terraform types The `terraform` types folder includes a representation of your data models that is appropriate for the `terraform-plugin-framework` type system. However, if you have multiple types with the same **signature** (for example, the same set of child property **types**), a lot of these types may be effectively duplicated. To minimize the Git repository and binary size, it might make sense to deduplicate these types by reusing types with the same signature across different resources. To enable this, set the following configuration option: ```yaml terraform: enableTypeDeduplication: true ``` This option is `false` by default. # Common troubleshooting and recipes Source: https://speakeasy.com/docs/terraform/customize/common-troubleshooting When generating Terraform providers from OpenAPI documents, you might encounter cases where your API's structure doesn't naturally fit Terraform's resource-oriented approach. These differences typically occur because APIs aren't always designed with Terraform's infrastructure management style in mind. Speakeasy's generator identifies these design differences and offers extensions and customization options to address them. This enables you to produce effective Terraform providers, even when your API doesn't initially match Terraform's requirements, simplifying what might otherwise be challenging configuration tasks. ## Fixing common API issues ### Impedance mismatch errors An impedance mismatch error happens when Speakeasy's generator finds properties with different data types that need to be combined into one. This error means the data types don't match up correctly across API operations (such as between request and response data or across different operations for the same entity), which can cause problems in the Terraform provider being created. A typical impedance mismatch scenario occurs in the following situation: - A request takes a UUID string for task assignment: ```json filename="request.json" { "taskId": "12345", "assignee": "user-uuid-1" } ``` - The response returns a full user object for that assignee: ```json filename="response.json" { "taskId": "12345", "assignee": { "id": "user-uuid-1", "name": "Alice Johnson" } } ``` When the generator attempts to merge these properties, it detects that it cannot reconcile the different data types (string vs object) and yields an impedance mismatch error. ### How to fix an impedance mismatch error Speakeasy includes built-in extensions to help fix impedance mismatch errors. You can use these solutions to adjust either the request or the response data: #### Option 1: Override the property name Use `x-speakeasy-name-override` to give the mismatched properties different names so they no longer attempt to merge: ```yaml filename="openapi.yaml" TaskRequest: type: object x-speakeasy-entity: Task properties: taskId: type: string assignee: type: string x-speakeasy-name-override: assigneeId # Override the name to avoid merging ``` With this approach, the additional data (stored in the `assignee` response field) is still available to Terraform consumers. However, there may be a loss of drift detection when Terraform updates the resource's state during the read operation. **Note:** You can also use `x-speakeasy-name-override` to rename the conflicting property (in this case, "assignee") in the response schema to achieve the same effect. The key is to prevent a type collision between request and response. #### Option 2: Ignore the mismatched property Use `x-speakeasy-ignore` to exclude the problematic property from generation: ```yaml filename="openapi.yaml" TaskResponse: type: object x-speakeasy-entity: Task properties: taskId: type: string assignee: type: object properties: id: type: string name: type: string x-speakeasy-ignore: true # Ignore this property in the response ``` This approach resolves the impedance mismatch by removing the conflicting property altogether. However, this also means that the property is no longer tracked in Terraform state. # Map API Entities to Terraform Resources Source: https://speakeasy.com/docs/terraform/customize/entity-mapping import { Callout } from "@/mdx/components"; ## Entity Mapping Add the `x-speakeasy-entity` extension to objects in your OpenAPI Specification document to include them as entities in the Terraform provider, such as managed resources. The extension value may be a single string or an array of strings if the object should be represented by multiple API entities. As a component: ```yaml components: schemas: Order: description: An order helps you make coffee x-speakeasy-entity: Order properties: id: type: integer description: Numeric identifier of the order. name: type: string description: Product name of the coffee. price: type: number description: Suggested cost of the coffee. required: - name - price type: object ``` Or inline in a path: ```yaml paths: /order: post: tags: - Order summary: Create a coffee order x-speakeasy-entity-operation: Order#create requestBody: content: application/json: schema: x-speakeasy-entity: Order properties: id: type: integer description: Numeric identifier of the order. name: type: string description: Product name of the coffee. price: type: number description: Suggested cost of the coffee. required: - name - price type: object ``` ```hcl resource "yourprovider_order" "example" { name = "Filter Blend" price = 11.5 } ``` Where you place the `x-speakeasy-entity` annotation affects the Terraform resource schema structure. - **At the top level:** Properties are nested objects. - **At a lower level:** Properties above the annotation are flattened. ### Top Level ```yaml Pet: x-speakeasy-entity: Order type: object properties: data: type: object properties: name: type: string # ... ``` Results in the following resource schema and configuration: ```hcl resource "yourprovider_order" "example" { data = { name = "Filter Blend" } } ``` ### Lower Level ```yaml Pet: type: object properties: data: x-speakeasy-entity: Order type: object properties: name: type: string #... ``` Results in the following resource schema and configuration: ```hcl resource "yourprovider_order" "example" { name = "Filter Blend" } ``` Properties above the `x-speakeasy-entity` annotation are flattened, which could cause conflicts. Apply the annotation carefully to align the structure of the Terraform provider with the API's intended interaction. ## Specify CRUD Operations for API Endpoints The `x-speakeasy-entity-operation` annotation specifies CRUD (create, read, update, and delete) operations associated with each endpoint in the OpenAPI spec for a Terraform entity. The value determines the behavior of operations such as create, read, update, and delete and is structured as `Entity#operation,operation,...#order`: - `Entity` represents the name of the entity. - `operation` can be one or more of `create`, `read`, `update`, and `delete`, concatenated with commas. - `order` is optional and can be used to define additional API calls that should be invoked for a given CRUD invocation. ### Behavior of Operations - `Entity#create` makes the entity a Terraform resource. - `Entity#read` ensures consistency with Terraform state, updates attributes, and generates a data source. - `Entity#update` provides update support for the resource. Without it, any attribute change requires resource replacement (`ForceNew`). - `Entity#delete` enables deletion of the resource. Without it, no action is taken on deletion. - `Entity#create,update` **(idempotent operations)** indicates the API is idempotent. Combine these operations to allow the same API call to create new objects and update existing ones, depending on attribute changes. In this example, a Pet managed resource with full create, read, update, and delete lifecycle and Pet data resource (due to `Pet#read`) are defined: ```yaml paths: /pet: post: tags: - pet summary: Add a new pet to the store x-speakeasy-entity-operation: Pet#create /pet/{petId}: get: tags: - pet summary: Info for a specific pet x-speakeasy-entity-operation: Pet#read put: tags: - pet summary: Update the pet x-speakeasy-entity-operation: Pet#update delete: tags: - pet summary: Delete the pet x-speakeasy-entity-operation: Pet#delete ``` Terraform generation automatically handles pagination implementation details via the `x-speakeasy-pagination` [extension](/docs/customize/runtime/pagination). This includes paging through all responses and removing unnecessary pagination handling properties from the schema. In this example, an automatically paginated Pets data resource is defined: ```yaml paths: /pet: get: tags: - pet summary: Lists all pets x-speakeasy-entity-operation: Pets#read x-speakeasy-pagination: type: offsetLimit inputs: - name: page in: parameters type: page outputs: results: $.results ``` ### Multiple API Operations for One Resource When multiple API operations are necessary for a single resource, use the additional entity-ordering capabilities of the `x-speakeasy-entity-operation` annotation. ```yaml paths: /pet/{petId}: get: x-speakeasy-entity-operation: Pet#read#1 /animal: get: x-speakeasy-entity-operation: Pet#read#2 ``` Multiple API operations for one resource can be combined with multiple entity operations of [one API operation for multiple resources](#one-api-operation-for-multiple-resources) as necessary. If additional API operations have transient properties, such as an asynchronous task identifier necessary for polling, use `x-speakeasy-terraform-ignore: schema` to remove them from the Terraform schema. ### One API Operation for Multiple Resources When a single API operation is necessary for multiple resources, use multiple entity operation entries with the `x-speakeasy-entity-operation` annotation. ```yaml parameters: - in: query name: id required: false schema: type: string operationId: GetAnimal x-speakeasy-entity-operation: - Cat#read - Dog#read ``` One API operation for multiple resources can be combined with the entity operation ordering of [multiple API operations for one resource](#multiple-api-operations-for-one-resource) as necessary. If the resource creation requires both a create and update operation where the create response includes an API assigned resource identifier, mark the update request resource identifier property as read only via `x-speakeasy-param-readonly: true` to ensure the property remains non-configurable in the schema. ### API Operation Polling Define automatic API operation polling logic for APIs with asynchronous behaviors via the `x-speakeasy-polling` and `x-speakeasy-entity-operation` extensions in the OpenAPI Specification document. Refer to the [SDK Polling documentation](/docs/customize/runtime/polling) for additional information about configuring `x-speakeasy-polling`. In this example: ```yaml /task: post: x-speakeasy-entity-operation: Task#create#1 /task/{id}: get: responses: "200": description: OK content: application/json: schema: type: object properties: name: type: string status: type: string enum: - completed - errored - pending - running required: - name - status x-speakeasy-polling: - name: WaitForCompleted failureCriteria: - condition: $statusCode == 200 - condition: $response.body#/status == "errored" successCriteria: - condition: $statusCode == 200 - condition: $response.body#/status == "completed" x-speakeasy-entity-operation: - Task#read - entityOperation: Task#create#2 options: polling: name: WaitForCompleted ``` The API operation is used for both the read operation and the second create operation, where the second create operation will use the `WaitForCompleted` polling method to ensure the success criteria is met before the resource logic (and therefore Terraform) continues. There are `delaySeconds`, `intervalSeconds`, and `limitCount` configurations to override the `x-speakeasy-polling` configuration values for a specific entity operation. In this example: ```yaml /task/{id}: get: x-speakeasy-polling: - name: WaitForCompleted failureCriteria: - condition: $statusCode == 200 - condition: $response.body#/status == "errored" intervalSeconds: 5 successCriteria: - condition: $statusCode == 200 - condition: $response.body#/status == "completed" x-speakeasy-entity-operation: - Task#read - entityOperation: Task#create#2 options: polling: name: WaitForCompleted intervalSeconds: 10 ``` The `WaitForCompleted` polling method for the API operation defaults to a 5 second interval, however the create entity operation overrides to a 10 second interval. ### Update Patch Semantics Define automatic patch semantics for Terraform provider update operations via the `x-speakeasy-entity-operation` extension in the OpenAPI Specification document. When enabled, entity operations will only send attributes that have changed from their prior state, rather than sending the entire resource representation. This is particularly useful for APIs that support partial updates (such as PATCH semantics) where sending unchanged fields may cause unintended side effects or where bandwidth efficiency is important. #### Basic Usage In this example: ```yaml /resource: post: x-speakeasy-entity-operation: - entityOperation: Resource#create,update options: patch: style: only-send-changed-attributes operationId: create-or-update-resource requestBody: required: true content: application/json: schema: $ref: '#/components/schemas/ResourceRequest' responses: '200': description: OK content: application/json: schema: $ref: '#/components/schemas/ResourceResponse' ``` The `create,update` entity operation uses the `only-send-changed-attributes` patch style. During entity operations, the generated Terraform provider will compare each attribute against its prior state and only include attributes that have changed in the API request. #### Configuration The patch style is configured within the `options` section of an entity operation mapping: ```yaml x-speakeasy-entity-operation: - entityOperation: Entity#operation options: patch: style: only-send-changed-attributes ``` #### When to Use Use `only-send-changed-attributes` when: - Your API supports partial updates (PATCH semantics) and may have side effects when unchanged fields are sent - You want to minimize request payload size by excluding unchanged attributes - Your API has fields where re-sending the same value triggers unwanted behavior (such as regenerating tokens or timestamps) ### Manual association between Operations and Resource / Data Sources The default behavior within Speakeasy is to automatically infer a data source from all operations that have an `x-speakeasy-entity-operation: Entity#read` association defined. For some APIs, you might want the data source to use a "search" endpoint (e.g., search for an entity by name, where name is non-unique), while using a "get" operation for the resource (e.g., to find an entity by ID for state reconciliation). In this case, use an object syntax for the `x-speakeasy-entity-operation` annotation to explicitly control whether an operation generates a resource, a data source, or both: ```yaml paths: "/example": get: operationId: getThing x-speakeasy-entity-operation: terraform-datasource: null terraform-resource: Thing#read ``` This syntax allows you to: - Prevent automatic generation of a data source by setting `terraform-datasource` to `null` - Prevent invocation of the operation during the resource's Read method ("invoked as part of terraform state refresh") by setting `terraform-resource` to `null` For example, the configuration above declares that `getThing` is associated with just a resource, and a data source should not be automatically generated. ### Wrapping Additional API Operation Response Data When defining multiple API operations for a single entity, an API definition may be written such that those API operation response are a flattened object. When adding those additional operations to the entity, those flattened object properties are added to the top level of the resource schema by default. Use `x-speakeasy-wrapped-attribute` extension to override this behavior, which will create a wrapping attribute that contains the underlying object properties in the final resource schema. In this example, the resource will put the second API operation response properties underneath a `subconfig` attribute: ```yaml paths: /example/{id}: parameters: - name: id in: path required: true schema: type: string get: x-speakeasy-entity-operation: Example#read responses: "200": description: OK content: application/json: schema: $ref: "#/components/schemas/ExampleResponse" /example/{id}/subconfig: parameters: - name: id in: path required: true schema: type: string get: x-speakeasy-entity-operation: Example#read#2 responses: "200": description: OK content: application/json: schema: allOf: - $ref: "#/components/schemas/ExampleSubconfigResponse" - x-speakeasy-wrapped-attribute: subconfig ``` ### Array Response Wrapping Terraform resources require an object-type root schema. When an API returns an array, Speakeasy automatically wraps it in an attribute (default name: `data`). **Default behavior:** ```yaml paths: /things: get: x-speakeasy-entity-operation: Things#read responses: "200": description: OK content: application/json: schema: type: array items: x-speakeasy-entity: Things ``` Since the response is `type: array`, Speakeasy wraps it in a `data` attribute for Terraform compatibility. Access in Terraform: ```hcl data.example_things.data[0].id ``` **Customize the wrapper name:** ```yaml paths: /things: get: x-speakeasy-entity-operation: Things#read responses: "200": description: OK content: application/json: schema: x-speakeasy-wrapped-attribute: items type: array items: x-speakeasy-entity: Things ``` Now the wrapping attribute is named `items` instead: ```hcl data.example_things.items[0].id ``` The wrapping attribute name is a Terraform construct created by Speakeasy, completely separate from your API's response structure. Use `x-speakeasy-wrapped-attribute` to customize it. ### Resources with Soft Delete By default, a generated managed resource uses the HTTP 404 Not Found status code on read to automatically remove the resource from the Terraform state which causes the next Terraform plan to propose recreating the resource. For resource APIs that support soft delete (grace time period before the resource is fully deleted), the `x-speakeasy-soft-delete-property` annotation adds a check against a read response property to also propose resource recreation. For managed resources, any `x-speakeasy-soft-delete-property` attribute is omitted from the schema and state. For data resources, the attribute remains to preserve client-side filtering capabilities. In this example, the resource will be proposed for recreation if the `deleted_at` property has a value: ```yaml paths: "/example": get: x-speakeasy-entity-operation: Example#read responses: "200": description: OK content: application/json: schema: $ref: "#/components/schema/ExampleGetResponse" components: schemas: ExampleGetResponse: type: object properties: # ... deleted_at: type: string format: date-time x-speakeasy-soft-delete-property: true ``` # Note Source: https://speakeasy.com/docs/terraform/customize/extensions Speakeasy reorganized the Terraform documentation for better navigation. Visit the [new documentation structure](/docs/terraform) to find all Terraform-related content. # Customize Your Terraform Provider Source: https://speakeasy.com/docs/terraform/customize Speakeasy provides various extensions and configurations to customize your Terraform provider. These customizations allow you to: - Map API entities and operations to Terraform resources - Enable provider-level configurations and environment values - Customize validation and plan modification - And more Select a topic from the navigation to learn more about specific customization options. ## Available Customization Options ### [Entity Mapping](/docs/terraform/entity-mapping) Learn how to map your API entities to Terraform resources and specify CRUD operations. ### [Provider Configuration](/docs/terraform/provider-configuration) Configure environment values and manage custom resources. ### [Resource Configuration](/docs/terraform/resource-configuration) Configure resource descriptions and managed resource versioning. ### [Property Customization](/docs/terraform/property-customization) Customize how API properties are mapped to Terraform attributes and manage property behavior. ### [Validation and Dependencies](/docs/terraform/validation-dependencies) Add custom validation logic and manage attribute dependencies. ### [Plan Modification](/docs/terraform/plan-modification) Customize Terraform plan behavior and resource versioning. ### [Advanced Features](/docs/terraform/advanced-features) Access advanced customization options for fine-grained control. ### [Schema Keywords](/docs/terraform/schema-keywords) Learn about supported OpenAPI schema keywords and their behavior. ### [Common Troubleshooting & Recipes](/docs/terraform/common-troubleshooting) Learn about overcoming API design incompatibilities when generating Terraform providers with Speakeasy. ### [Configuration Reference](/docs/speakeasy-reference/generation/terraform-config) Complete reference for all available Terraform configuration options in the `gen.yaml` file. # Plan Modification Source: https://speakeasy.com/docs/terraform/customize/plan-modification import { Callout } from "@/mdx/components"; ## Custom Attribute Plan Modification Attribute plan modifiers enable advanced default value, resource replacement, and difference suppression logic in managed resources. Due to the Terraform SDK implementation, attribute-level plan modifiers do not have access to provider-level configuration or the API client, however that SDK does support custom resource-level plan modification with implementing the [`resource.ResourceWithModifyPlan` interface](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-framework/resource#ResourceWithModifyPlan). Use the `x-speakeasy-plan-modifiers` extension to add custom attribute-level plan modification logic to Terraform plan operations. ```yaml components: schemas: Pet: type: object x-speakeasy-entity: Pet properties: name: type: string age: type: integer x-speakeasy-plan-modifiers: AgeModifier ``` Once you've added the `x-speakeasy-plan-modifiers` extension with a modifier, Speakeasy's next Terraform provider generation will bootstrap a custom plan modifier file. Using the previous YAML snippet as an example, the modifier will be located at `internal/planmodifiers/int64planmodifier/age_modifier.go`, and import the schema configuration wherever `x-speakeasy-plan-modifiers: AgeModifier` is referenced. The `x-speakeasy-plan-modifiers` extension supports an array of names as well, such as: ```yaml x-speakeasy-plan-modifiers: - FirstPlanModifier - SecondPlanModifier ``` ### Implementation Notes A plan modifier is a type that implements the plan modifier interface defined by the `terraform-plugin-framework`. A unique plan modifier is bootstrapped in the appropriate subfolder for the Terraform type that it is applied to, which is usually one of the following: - `boolplanmodifiers` - `float64planmodifiers` - `int64planmodifiers` - `listplanmodifiers` - `mapplanmodifiers` - `numberplanmodifiers` - `objectplanmodifiers` - `setplanmodifiers` - `stringplanmodifiers` 4. A modifier can only be applied to a resource attribute. The annotation will be ignored for data sources. Modifiers cannot be applied at the same level as the `x-speakeasy-entity` annotation because that becomes the "root" of the Terraform resource. 5. Speakeasy regenerations do not delete user-written code. If the modifier is no longer in use, it will be ignored (no longer referenced) but the source file will remain. You might want to delete such an orphaned modifier file for repository hygiene. # Customize Terraform Properties Source: https://speakeasy.com/docs/terraform/customize/property-customization import { Callout } from "@/mdx/components"; ## Remap API Property to Terraform Attribute Name The `x-speakeasy-name-override` annotation adjusts the Terraform attribute name within a resource while remapping all the API data handling internally. This is useful, for example, to standardize differing API property names across operations to a single attribute name. ```yaml unique_id: type: string x-speakeasy-name-override: id ``` The annotation also has other [SDK customization capabilities](/docs/customize-sdks/methods), however, those are generally unnecessary for Terraform providers as the generated Go SDK is internal to the provider code. ## Align API Parameter With Terraform Property The `x-speakeasy-match` annotation adjusts the API parameter name to align with a Terraform state property. If mismatches occur, a generation error will highlight appropriate root-level properties for accurate mapping. ```yaml paths: /pet/{petId}: delete: parameters: - name: petId x-speakeasy-match: id x-speakeasy-entity-operation: Pet#delete ``` ## Customize Status Codes for Missing Resources By default, Terraform removes a resource from state when a Read operation returns an HTTP 404 Not Found status code. However, some APIs use different status codes to indicate a resource is missing or has been deleted, such as 403 Forbidden or 410 Gone. The `x-speakeasy-entity-missing-codes` extension allows you to specify additional HTTP status codes that should trigger resource removal during Read operations. Apply this extension at the operation level on Read endpoints. ```yaml paths: "/pet/{petId}": get: x-speakeasy-entity-operation: Pet#read x-speakeasy-entity-missing-codes: - 403 - 410 parameters: - name: petId in: path required: true schema: type: string responses: "200": description: Successful response content: application/json: schema: $ref: "#/components/schemas/Pet" "403": description: Forbidden - resource has been deleted "404": description: Not found "410": description: Gone - resource permanently deleted ``` When any of the specified status codes are returned during a Read operation, Terraform will automatically remove the resource from state and propose recreation on the next plan. The 404 status code is always checked by default, so you only need to specify additional codes. ## Property Defaults Setting a property default value that matches your API responses when unconfigured will enhance the Terraform plan to include the known value, rather than propagate the value as unknown `(known after apply)` during creation and updates. Speakeasy generation automatically adds a Terraform schema attribute default with OAS `default` value, for each of the following OAS types: | OAS Schema Type | OAS default Support | | --------------- | ------------------------------------------------------------------------ | | `array` | Partial (`[]` and values of `boolean`/`number`/`string` item types only) | | `boolean` | Yes | | `map` | No | | `number` | Yes | | `object` | Partial (`null` only) | | `oneOf` | No | | `string` | Yes | ### Custom Defaults For unsupported or advanced use cases, the [Terraform SDK supports calling schema-defined custom default logic](https://developer.hashicorp.com/terraform/plugin/framework/resources/default#custom-default-implementations). Create the custom code implementing the [Terraform type-specific `resource/schema/defaults` package interface](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-framework/resource/schema/defaults) in any code location and use the OAS `x-speakeasy-terraform-custom-default` extension to reference that implementation in the schema definition. In this example, a custom string default implementation is created in `internal/customdefaults/example.go`: ```go package customdefaults import ( "context" "github.com/hashicorp/terraform-plugin-framework/resource/schema/defaults" "github.com/hashicorp/terraform-plugin-framework/types" ) func Example() defaults.String { return exampleDefault{} } type exampleDefault struct{} func (d exampleDefault) Description(ctx context.Context) string { return "Example custom default description" } func (d exampleDefault) MarkdownDescription(ctx context.Context) string { return "Example custom default description" } func (d exampleDefault) DefaultString(ctx context.Context, req defaults.StringRequest, resp *defaults.StringResponse) { resp.PlanValue = types.StringValue("example custom default") } ``` With the following OAS configuration on the target property: ```yaml example: type: string x-speakeasy-terraform-custom-default: imports: - github.com/examplecorp/terraform-provider-examplecloud/internal/customdefaults schemaDefinition: customdefaults.Example() ``` The `imports` configuration is optional if the custom code is within the `internal/provider` package and does not require additional imports. ## Hide Sensitive Properties Properties marked as `x-speakeasy-param-sensitive` will be concealed from the console output of Terraform. This helps to ensure the confidentiality of sensitive data within Terraform operations. ```yaml components: schemas: Pet: type: object properties: name: type: string secret: type: string x-speakeasy-param-sensitive: true ``` ## Write only Mark a schema attribute as a [write only argument](https://developer.hashicorp.com/terraform/plugin/framework/resources/write-only-arguments) (`WriteOnly: true` in the schema definition) with the `x-speakeasy-terraform-write-only` extension. Write only functionality prevents values from ever being exposed in plan or state data as a more complete secret value solution over sensitive attributes. Write only functionality is only supported in Terraform/OpenTofu 1.11 and later. Using it on earlier versions of Terraform/OpenTofu will result in a configuration error. ```yaml components: schemas: Pet: type: object properties: name: type: string secret: type: string x-speakeasy-terraform-write-only: true ``` ## Deprecation Add OAS `deprecated: true` within a property to automatically return a warning diagnostic with a generic deprecation message when the property is configured in Terraform. Customize the messaging with the OAS `x-speakeasy-deprecation-message` extension. Terraform always returns deprecation warnings for configured properties, but has limitations for displaying these warnings with response-only properties that are referenced elsewhere in the configuration. [Terraform Feature Request](https://github.com/hashicorp/terraform/issues/7569) In this example, Terraform will display a warning diagnostic with `Custom deprecation message` if the property is configured: ```yaml example: type: string deprecated: true x-speakeasy-deprecation-message: Custom deprecation message ``` ## Exclude Property From Terraform State When `x-speakeasy-terraform-ignore: true`, this extension ensures the specified property and any interactions involving it are omitted from Terraform's state management. This extension completely suppresses the property from the Terraform state. If you want to suppress a specific operation, use `x-speakeasy-ignore: true` to omit the operation from the annotated CRUD method. For example, if a field is present in both the `CREATE` and `READ` response bodies, omitting it from the `READ` response body will turn off drift detection for that field. The field will remain in the `CREATE` response body and the Terraform state. ```yaml components: schemas: Pet: x-speakeasy-entity: Pet type: object properties: optionalMetadata: x-speakeasy-terraform-ignore: true type: string name: type: string required: - name ``` ```hcl resource "petstore_pet" "mypet" { name = "myPet" # Attempting to set an ignored parameter results in an error # optionalMetadata = true } ``` Use `x-speakeasy-terraform-ignore: schema` to prevent generation errors when the property is required across API operations for the same Terraform operation and it should only be removed from the Terraform schema/state. ## Custom Types Set the `x-speakeasy-terraform-custom-type` extension to switch a property from the terraform-plugin-framework base type (e.g. `types.String`) to a [custom type](https://developer.hashicorp.com/terraform/plugin/framework/handling-data/types/custom). Custom types typically include format-specific validation logic (such as a baked-in regular expression) or semantic equality handling to prevent unintentional value differences (such as ignoring inconsequential whitespace). The following terraform-plugin-framework base types are supported for custom types: - `Bool` - `Float32` - `Float64` - `Int32` - `Int64` - `List` - `Map` - `Set` - `String` In this example, the `ipv4_address` string property will use the custom `iptypes.IPv4Address` type: ```yaml ipv4_address: type: string x-speakeasy-terraform-custom-type: imports: - github.com/hashicorp/terraform-plugin-framework-nettypes/iptypes schemaType: "iptypes.IPv4AddressType{}" valueType: iptypes.IPv4Address ``` ## Allow JSON String Attributes Set the `x-speakeasy-type-override` extension to `any` to convert the associated attribute to a JSON string. This allows for inline the specification of the attribute's value, accommodating attributes with variable or dynamic structures. ```yaml components: schemas: Pet: x-speakeasy-entity: Pet type: object properties: deep: x-speakeasy-type-override: any type: object properties: object: type: object additionalProperties: true properties: in: type: object properties: here: type: string name: type: string required: - name ``` ```hcl resource "petstore_pet" "mypet" { name = "myPet" deep = jsonencode({ object = { with = "anything" defined = true } }) } ``` ## Suppress Unnecessary Plan Changes Setting the `x-speakeasy-param-suppress-computed-diff` to true suppresses unnecessary Terraform plan changes for computed attributes that are not definitively known until after application. This is useful in scenarios where computed attributes frequently cause spurious plan changes. ```yaml components: schemas: Pet: x-speakeasy-entity: Pet type: object properties: name: type: string status: x-speakeasy-param-suppress-computed-diff: true type: string ``` Applying this modifier when `x-speakeasy-entity-operation: my_resource#read` is not defined may result in drift between the Terraform plan and remote state should updates to attributes happen outside of Terraform changes. Please only apply this when necessary. # Provider configuration Source: https://speakeasy.com/docs/terraform/customize/provider-configuration import { Callout } from "@/mdx/components"; import { Table } from "@/mdx/components"; ## Security The generated Terraform Provider will automatically implement all global security, as defined in the OpenAPI Specification, via the root-level `security` property and its associated `securitySchemes` components. Multiple security options are supported.
In this example, the generated provider requires a bearer authentication token using an `access_token` provider-level attribute: ```yaml components: securitySchemes: accessToken: type: http scheme: bearer security: - accessToken: [] ``` Refer to the [Configuring environment variables](#configuring-environment-variables) section to optionally enable the fallback environment variable configuration of the value. Operation-level security is also supported where it is implemented per-resource, however it is not recommended. Instead, we recommend implementing separate providers for each security layer. Terraform practitioners are conventionally used to the layering of provider implementations for this use case, and Terraform itself is designed around this separation of concerns. Refer to the [HashiCorp provider design principles](https://developer.hashicorp.com/terraform/plugin/best-practices/hashicorp-provider-design-principles) for more context. ## Server URL Choose how the generated Terraform Provider will handle the server URL for API requests: - Hardcoded server URL in Speakeasy generation configuration (`gen.yaml` file `generation` section `baseServerUrl` property) and not configurable in Terraform configurations. - As defined in the OpenAPI Specification, via the root-level `servers` property. The generation will automatically handle: - Creating a `server_url` attribute that defaults to the first defined `url` value and is configurable in Terraform configurations. - Creating server URL variable attributes that default to their defined `default` values and are configurable in Terraform configurations. It is recommended to use a single server URL for Terraform generation. Setting up multiple server URLs will create potentially confusing configuration options for Terraform consumers since variables across all server URLs are exposed. In this example, the provider configuration is generated with configurable `location` and `server_url` attributes: ```yaml servers: - url: https://{location}.example.com variables: location: default: production ``` Refer to the [Configuring environment variables](#configuring-environment-variables) section to optionally enable the fallback environment variable configuration of the value(s). Path-level and operation-level servers are also supported where it is implemented per-resource, however it is not recommended. Instead, we recommend implementing separate providers for each server layer. Terraform practitioners are conventionally used to the layering of provider implementations for this use case, and Terraform itself is designed around this separation of concerns. Refer to the [HashiCorp provider design principles](https://developer.hashicorp.com/terraform/plugin/best-practices/hashicorp-provider-design-principles) for more context. ## Globals Use the [`x-speakeasy-globals` extension](/docs/customize/globals) to enable provider-level configuration of common properties across multiple resources. This customization allows Terraform practitioners to configure a value in three ways: - **Provider-level only:** The default value is applied to any resources that use the global. - **Resource-level only:** The explicit value is applied only to those resource instance(s). - **Provider-level with resource-level override:** The default value is applied to any resources that use the global, but any explicit resource-level configurations override the provider-level value. In this example, the provider will accept an `organization_id` configuration as a global: ```yaml x-speakeasy-globals: parameters: - name: organizationId in: path schema: type: string ``` ## Configuring environment variables Use the `environmentVariables` configuration in the `gen.yaml` to set up an environment variable fallback for configuring provider attribute data. For example, the fallback may be accepting an access token value via an environment variable, rather than requiring an explicit `provider` block attribute configuration from Terraform practitioners. ```yaml terraform: environmentVariables: - env: EXAMPLE_SERVER_URL_FROM_ENV_VAR providerAttribute: server_url - env: EXAMPLE_ACCESS_TOKEN providerAttribute: access_token ``` The `environmentVariables` configuration is expected to be a list of objects with `{env: string, providerAttribute: string}` keys and values. These associate environment variables (referenced as `env`) with provider attributes (referenced as `providerAttribute`). ## Additional provider configurations Use the `additionalProviderAttributes` configuration in the `gen.yaml` file to enable Terraform configurations to specify additional provider-wide customizations. For example: ```yaml terraform: additionalProviderAttributes: # ... configuration ... ``` ### Custom HTTP headers Set the `httpHeaders` configuration with the desired attribute name to enable Terraform configurations to map additional HTTP headers for all HTTP requests. In this example, HTTP header customization is enabled using the `http_headers` provider attribute name: ```yaml terraform: additionalProviderAttributes: httpHeaders: http_headers ``` This configuration enables a provider configuration, such as: ```hcl provider "examplecloud" { http_headers = { "X-Example-Header" = "example-value" } } ``` ### Skip TLS verification Set the `tlsSkipVerify` configuration with the desired attribute name to enable Terraform configurations to specify a Boolean to disable TLS verification in the HTTP client. In this example, TLS verification customization is enabled using the `tls_skip_verify` provider attribute name: ```yaml terraform: additionalProviderAttributes: tlsSkipVerify: tls_skip_verify ``` This configuration enables a provider configuration, such as: ```hcl provider "examplecloud" { tls_skip_verify = true } ``` ## Custom resources or data sources To include an existing resource that is outside of the Speakeasy-generated provider, reference it in `gen.yaml` as follows: ```yaml terraform: additionalResources: - importAlias: custom importLocation: github.com/custom/terraform-provider-example/src/custom_resource resource: custom.NewCustomResource additionalDataSources: - importAlias: custom importLocation: github.com/custom/terraform-provider-example/src/custom_datasource datasource: custom.NewCustomDataSource additionalEphemeralResources: - importAlias: custom importLocation: github.com/custom/terraform-provider-example/src/custom_ephemeral_resource resource: custom.NewCustomEphemeralResource additionalListResources: - importAlias: custom importLocation: github.com/custom/terraform-provider-example/src/custom_list_resource resource: custom.NewCustomListResource ``` The `additionalResources` key is expected to contain a list of `{ importLocation?: string, importAlias?: string, resource: string }` objects. Each `resource` is inserted into the provider-managed resource list. If `importLocation` or `importAlias` is defined, Speakeasy adds them to the import list at the top of the provider file. The value of `resource` is arbitrary text, and could contain a function invocation if desired. The `additionalEphemeralResources` and `additionalListResources` keys follow the same syntax, but insert ephemeral resources and list resources into the provider respectively. The `additionalDataSources` key follows the same syntax, but inserts data resources into the provider using `datasource` (instead of `resource`) as the value inserted into the list. To learn more about how to write a Terraform resource, please consult the [official Terraform documentation](https://developer.hashicorp.com/terraform/plugin/framework). # Resource configuration Source: https://speakeasy.com/docs/terraform/customize/resource-configuration import { Table, Callout } from "@/mdx/components"; ## Resource documentation Speakeasy automatically generates provider and resource documentation that is compliant with the [public Terraform Registry requirements](https://developer.hashicorp.com/terraform/registry/providers/docs) via the HashiCorp-maintained [`terraform-plugin-docs`](https://github.com/hashicorp/terraform-plugin-docs) tool. Speakeasy runs `terraform-plugin-docs` at the end of successful generations or manually run via `go generate ./...`. The public Terraform Registry and `terraform-plugin-docs` tool both follow specific file layout conventions. For resources, the following file conventions are used:
The `terraform-plugin-docs` tool also supports its own advanced use case customization, such as custom templates. Refer to the [`terraform-plugin-docs` documentation](https://github.com/hashicorp/terraform-plugin-docs) for more information about those capabilities. ### Resource Description The `x-speakeasy-entity-description` extension modifies the description of a Terraform data or managed resource. This is useful when augmenting the documentation for specific resources in an OpenAPI document. This documentation is expected to be in Markdown format. Use this extension alongside the `x-speakeasy-entity` extension. In this example, an order managed resource will have `Manage a coffee order.` written as the description in the resource code for any consuming tools, including documentation written by `terraform-plugin-docs` into `docs/resources/order.md`: ```yaml components: schemas: Order: description: An order helps you make coffee x-speakeasy-entity: Order x-speakeasy-entity-description: | Manage a coffee order. ``` ### Resource Example Example resource configuration is based on the OpenAPI Specification for API operation request and response data schemas. Speakeasy only generates example configuration for configurable properties. Example configuration values use the property `example` field when available or, as a fallback, use a generated value of the expected type following any `enum` or validation fields. Fully customize the example resource configuration by using Speakeasy [code customization capabilities](/docs/customize/code/monkey-patching). For example, to manually manage and update the example configuration for a managed resource: * Edit root directory `.genignore` file with an entry for `examples/resources/{type}/resource.tf` * Edit `examples/resources/{type}/resource.tf` as necessary * Run `go generate ./...` or Speakeasy generation to update `docs/resources/{name}.md` ## Resource version State upgraders should be seldomly used for breaking schema type changes for existing state data. For adding/removing attributes, state upgraders are not needed and should be avoided. The `x-speakeasy-entity-version` extension specifies the version of a given resource and should **only** be used if you need to write a state migrator, for instance, if you are changing the type of a field. Terraform resource versions are zero-indexed and default to `0`. For your first breaking change requiring a state migrator, set `x-speakeasy-entity-version: 1`. Each state migrator function must migrate from the previous version of the state. If this is set, a boilerplate state upgrader will be written and hooked into `internal/stateupgraders/your_resource_v1.go`. Please refer to the [Terraform documentation](https://developer.hashicorp.com/terraform/plugin/framework/resources/state-upgrade) for guidance on writing a state migrator. # Schema keywords Source: https://speakeasy.com/docs/terraform/customize/schema-keywords import { Callout } from "@/mdx/components"; This section is not an exhaustive list of available keyword options. If you're unsure whether a keyword is supported, please reach out to our team at support@speakeasy.com. ### The anyOf keyword Terraform has limited support for the `anyOf` keyword due to its type system, which is less flexible than the JSON Schema type system. For instance, managing `anyOf` with multiple subtypes requires a large set of combined types, leading to practical and implementation challenges. Consider replacing `anyOf` in the schema with `oneOf` or `allOf`. This adjustment aligns with Terraform's capabilities, namely, using `oneOf` for union types and `allOf` for intersection types. For more guidance or to discuss schema adaptations, contact our support team at support@speakeasy.com. ### The oneOf keyword In Terraform, the `oneOf` keyword is defined as a `SingleNestedAttribute`, where each potential child is represented by a unique key. To ensure compliance with `oneOf` semantics, Speakeasy adds `conflicts-with` plan validators to confirm that only one of these keys is active at any given time. If a `oneOf` keyword is declared at the root level of an entity, the Speakeasy generator extracts common property attributes and duplicates them into the root level. This is important if, for instance, a common `id` property is required for making read, update, or delete requests. ### The allOf keyword For the `allOf` keyword, Speakeasy merges all subschemas into a single combined attribute, creating a unified schema component that encapsulates all specified properties. # Terraform Testing Source: https://speakeasy.com/docs/terraform/customize/testing import { Callout } from "@/mdx/components"; Testing a Terraform Provider is critical for ensuring your customers are successfully able to write configurations that apply consistently without errors or unexpected behaviors. There are two categories of Terraform Provider testing: - Manual: Locally install the Terraform Provider with Terraform CLI configuration provider development overrides and run Terraform commands with individual configurations. - Automated: Using native Go programming language testing functionality, automatically run Terraform configurations using real Terraform commands for provider code under development. It is recommended to implement automated testing to simplify development and verification over time. ## Prerequisites For both categories of testing, [install the Terraform CLI](https://developer.hashicorp.com/terraform/install) where the testing will be ran. Locally, install via a package manager or binary download. On GitHub Actions, install via the [`hashicorp/setup-terraform`](https://github.com/hashicorp/setup-terraform) action. ## Manual Testing After generating a Terraform Provider using Speakeasy, the README file includes instructions for how to manually test with [Terraform CLI provider development overrides](https://developer.hashicorp.com/terraform/cli/config/config-file#development-overrides-for-provider-developers). Those instructions are similar to the below. In the repository root directory, build the provider binary: ```shell go build . ``` Create or edit your [Terraform CLI configuration file](https://developer.hashicorp.com/terraform/cli/config/config-file), such as `~/.terraformrc`, to point to your provider directory: ```hcl provider_installation { dev_overrides { "registry.terraform.io/hashicorp/examplecloud" = "/path/to/terraform-provider-examplecloud" } direct {} } ``` Change to any directory containing Terraform configurations for your provider and run Terraform commands such as `terraform apply`. The output should include a warning about the provider development overrides from the `~/.terraformrc` configuration. ## Automated Testing Speakeasy intends to generate automated testing in the future, similar to SDKs. Over time, these steps will be automatically handled during generation. Automated tests take Terraform configuration(s) and then perform create, read, import, update, and delete actions against those using real Terraform commands. Automated testing also supports writing assertions against the Terraform plan or state per test step. This is accomplished through the HashiCorp-maintained [`github.com/hashicorp/terraform-plugin-testing` Go module](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-testing). Refer to the [Terraform Provider testing documentation](https://developer.hashicorp.com/terraform/plugin/testing) for full details about capabilities. There are a few steps required to get started: - Add `github.com/hashicorp/terraform-plugin-testing` dependency to Speakeasy generation so it is automatically downloaded and installed. - Create provider code to testing code mapping function. - Create resource test files and configurations. - Run automated testing. ### Add Dependency In `.speakeasy/gen.yaml`, add `github.com/hashicorp/terraform-plugin-testing` to the `terraform` section `additionalDependencies` configuration. View the [latest Go package documentation for `terraform-plugin-testing`](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-testing) to retrieve the latest version number. ```yaml terraform: # ... other configuration ... additionalDependencies: github.com/hashicorp/terraform-plugin-testing: v1.13.3 ``` ### Provider Mapping Function Create a shared function in `internal/provider/provider_test.go` to reference the provider during testing. Ensure the provider package import matches your Go module name and provider name in the mapping matches the provider type (short name). ```go package provider_test import ( "github.com/hashicorp/terraform-plugin-framework/providerserver" "github.com/hashicorp/terraform-plugin-go/tfprotov6" "github.com/examplecorp/terraform-provider-examplecloud/internal/provider" ) // Returns a mapping of provider type names to provider server implementations, // suitable for acceptance testing via the ProtoV6ProtocolFactories field. func testProviders() map[string]func() (tfprotov6.ProviderServer, error) { return map[string]func() (tfprotov6.ProviderServer, error){ "examplecloud": providerserver.NewProtocol6WithError(provider.New("test")()), } } ``` ### Resource Testing Files Resource testing code is conventionally written as a Go test file (`internal/provider/xxx_resource_test.go`) while configurations are in `internal/provider/testdata` directories named after the test. Create a resource test by creating a Go test file, such as `internal/provider/thing_resource_test.go`. Modify the configuration and checks as necessary to match the resource implementation. ```go package provider_test import ( "regexp" "testing" "github.com/hashicorp/terraform-plugin-testing/config" "github.com/hashicorp/terraform-plugin-testing/helper/acctest" "github.com/hashicorp/terraform-plugin-testing/helper/resource" "github.com/hashicorp/terraform-plugin-testing/knownvalue" "github.com/hashicorp/terraform-plugin-testing/statecheck" "github.com/hashicorp/terraform-plugin-testing/tfjsonpath" ) // Verifies the create, read, import, update, and delete lifecycle of the // `examplecloud_thing` resource. func TestThingResource_lifecycle(t *testing.T) { t.Parallel() randomName := "test-" + acctest.RandString(10) resourceAddress := "examplecloud_thing.test" resource.Test(t, resource.TestCase{ ProtoV6ProviderFactories: testProviders(), Steps: []resource.TestStep{ // Verifies resource create and read. { ConfigDirectory: config.TestNameDirectory(), ConfigVariables: config.Variables{ "name": config.StringVariable(randomName+"-original"), }, // Check computed values. ConfigStateChecks: []statecheck.StateCheck{ statecheck.ExpectKnownValue( resourceAddress, tfjsonpath.New("id"), knownvalue.StringRegexp(regexp.MustCompile(`^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$`)), ), }, }, // Verifies resource import. { ConfigDirectory: config.TestNameDirectory(), ConfigVariables: config.Variables{ "name": config.StringVariable(randomName+"-original"), }, ResourceName: resourceAddress, ImportState: true, ImportStateVerify: true, }, // Verifies resource update. { ConfigDirectory: config.TestNameDirectory(), ConfigVariables: config.Variables{ "name": config.StringVariable(randomName+"-updated"), }, }, // Testing framework implicitly verifies resource delete. }, }) } ``` Create the associated testing configuration in `internal/provider/testdata/TestThingResource_lifecycle/main.tf`: ```hcl variable "name" { type = string } resource "examplecloud_thing" "test" { name = var.name } ``` ### Run Automated Testing Ensure Speakeasy generation, such as `speakeasy run`, has been run at least once beforehand to ensure the `github.com/hashicorp/terraform-plugin-testing` dependency is downloaded and installed. Verify via `go.mod` file contents. Run automated testing via native Go programming language testing functionality, such as the `go test` command. The `github.com/hashicorp/terraform-plugin-testing` Go module requires the `TF_ACC` environment variable to set, conventionally to `1` (enabled). Depending on your provider configuration requirements, you may also need to set other security or server URL environment variables. Run the following commands to perform the automated testing: ```shell # required once per session export TF_ACC=1 # ... export any required provider configuration environment variables ... go test -count=1 -timeout=10m -v ./... ``` The testing library will handle all the underlying details to run the provider code, call Terraform commands, and verify assertions. Run individual tests with the `-run` flag, which accepts a regular expression pattern. For example: ```shell go test -count=1 -run='TestThingResource_lifecycle' -timeout=10m -v ./... ``` # Validation and Dependencies Source: https://speakeasy.com/docs/terraform/customize/validation-dependencies import { Callout } from "@/mdx/components"; ## Prevent Conflicting Attributes The `x-speakeasy-conflicts-with` extension indicates that a property conflicts with another, ensuring that certain combinations of properties are not set together. This is ideal for situations where certain attributes are mutually exclusive or setting one attribute invalidates another. ```yaml components: schemas: Pet: x-speakeasy-entity: Pet type: object properties: name: type: string name_prefix: type: string x-speakeasy-conflicts-with: name id: type: string generated_name_options: type: object properties: prefix: type: string x-speakeasy-conflicts-with: - ../name_prefix - ../name - ../id ``` ``` resource "example_pet" "happy_pet" { name = "Mrs Poppy" name_prefix = "Mrs" } ``` ```txt $ terraform plan │ Error: Invalid Attribute Combination │ │ with example_pet.happy_pet, │ on provider.tf line 39, in resource "example_pet" "happy_pet": │ 3: name_prefix = "test" │ │ Attribute "name" cannot be specified when "name_prefix" is specified ``` ## Enforce Mutually Exclusive Attributes (x-speakeasy-xor-with) The `x-speakeasy-xor-with` extension ensures that exactly one of the listed attributes must be configured at the same time. If multiple attributes are set simultaneously or if no attribute is set, Terraform plan validation fails. This differs from `x-speakeasy-conflicts-with` in that it requires exactly one attribute to be set, while `conflicts-with` allows zero or one attribute to be set. ```yaml components: schemas: Pet: x-speakeasy-entity: Pet type: object properties: this: type: string that: type: string another: type: string # user MUST configure exactly one of: this, that, or another x-speakeasy-xor-with: - ../this - ../that ``` ``` resource "example_pet" "happy_pet" { this = "value1" that = "value2" # Error: exactly one field must be set } ``` ```txt $ terraform plan │ Error: Invalid Attribute Combination │ │ with example_pet.happy_pet, │ on provider.tf line 2: │ 2: that = "value2" │ │ Exactly one of attributes [this, that, another] must be specified ``` ## Enforce Required Attribute Dependencies (x-speakeasy-required-with) The `x-speakeasy-required-with` extension ensures that when the annotated field is configured, all the specified dependent fields must also be configured. This is useful for enforcing that certain fields are always configured together. ```yaml components: schemas: Pet: x-speakeasy-entity: Pet type: object properties: name: type: string age: type: integer breed: type: string # when breed is set, name and age must also be set x-speakeasy-required-with: - ../name - ../age ``` ``` resource "example_pet" "happy_pet" { breed = "Labrador" # Error: name and age must also be set when breed is set } ``` ```txt $ terraform plan │ Error: Missing Required Attributes │ │ with example_pet.happy_pet, │ on provider.tf line 2: │ 2: breed = "Labrador" │ │ The following attributes must be configured when 'breed' is specified: [name, age] ``` ## OpenAPI Plan Validators Speakeasy automatically generates certain Terraform configuration value validation handlers based on your OpenAPI specification. When configuration validation is defined, Terraform raises invalid value errors before users can apply their configuration for a better user experience. By default, these OpenAPI specification properties are automatically handled: - For `string` types: `enum`, `maxLength`, `minLength`, and `pattern` - For `integer` types: `enum`, `minimum`, and `maximum` - For `array` types: `maxItems`, `minItems`, and `uniqueItems` For use cases not automatically handled, add custom validation logic or reach out to the team. ### Add Custom Validation Logic Use the `x-speakeasy-plan-validators` extension to add custom validation logic to Terraform plan operations and ensure configurations meet predefined criteria before execution. This extension is essential for scenarios requiring advanced validation logic that JSON Schema cannot accommodate. ```yaml components: schemas: Pet: type: object x-speakeasy-entity: Pet properties: name: type: string age: type: integer x-speakeasy-plan-validators: AgeValidator ``` Once you've added the `x-speakeasy-plan-validators` extension with a modifier, Speakeasy's next Terraform provider generation will bootstrap a custom validator file. Using the previous YAML snippet as an example, the modifier will be located at `internal/validators/int64validators/age_validator.go`, and import the schema configuration wherever `x-speakeasy-plan-validators: AgeValidator` is referenced. You can modify the validator file to contain your logic. #### Implementation Notes 1. A plan validator is a type conformant to the `terraform-plugin-framework` expected interface. A unique plan validator will be bootstrapped in the appropriate subfolder for the Terraform type it is applied to: `boolvalidators`, `float64validators`, `int64validators`, `listvalidators`, `mapvalidators`, `numbervalidators`, `objectvalidators`, `setvalidators`, or `stringvalidators`. Speakeasy will always create and use a file as `snake_case.go` for a given `x-speakeasy-plan-validators` value. 2. A plan validator operates on the raw (untyped) Terraform value types. However, you can convert a Terraform type to a value type Speakeasy manages (`type_mytype.go`) by using the included reflection utility. This is useful for applying validators to complex types like `list`, `map`, `object`, and `set`. 3. While working with a plan validator, you have the ability to perform various tasks, including initiating network requests. However, it's important to ensure that plan validations do not result in any unintended side effects. Please refer to [the HashiCorp guidance on plan validator development](https://developer.hashicorp.com/terraform/plugin/framework/validation) or reach out in our Slack if you have questions. 4. It is possible to have an array of plan validators, for example, `x-speakeasy-plan-validators: [MinAgeValidator, MaxAgeValidator]`. 5. A validator can only be applied to a resource attribute. Validators cannot be applied at the same level as the `x-speakeasy-entity` annotation because that becomes the "root" of the Terraform resource. However, validators can access or refer to any data in the entire resource (for an example, see the `x-speakeasy-conflicts-with` validator). The annotation will be ignored for data sources. 6. Speakeasy regenerations do not delete user-written code. If the validator is no longer in use, it will be ignored (no longer referenced) but the source file will remain. You might want to delete such an orphaned validation file for repository hygiene. # What is a Terraform Provider Source: https://speakeasy.com/docs/terraform/guides/crud A Terraform provider is a plugin that extends Terraform, allowing it to manage external resources such as cloud services. It serves as a mediator between Terraform and external APIs, using the [Terraform Plugin Protocol](https://developer.hashicorp.com/terraform/plugin/terraform-plugin-protocol) for communication. Terraform providers, built with the [terraform-plugin-framework](https://github.com/hashicorp/terraform-plugin-framework), include: 1. **Resources** and **Data Sources**: described using a Terraform Type Schema, which is a `map[string]Attribute`, where an Attribute could be _Primitive_, _Composite_, or _List_. 2. **Create**, **Read**, **Update** and **Delete** methods: the interface through which the provider interacts with an external resource (usually an API) to reconcile a desired terraform specification with the actual state of the resource. 3. **Plan Validators**: defined to enable the validation of a desired specification at Plan-time, without making API calls to the external resource. 4. **Plan Modifiers**: defined to enable custom semantics around the Terraform Type Schema, and how it is reconciled with the external state to make a Plan. 5. **Resource imports**: defined to enable Terraform Specifications to be generated from existing resources. ## A simple CRUD example Let's explore how to define a resource and map API operations to Terraform methods using annotations for CRUD actions. ### Defining a Resource Use `x-speakeasy-entity` to define a resource that you want to use terraform to manage. ```yaml filename="openapi.yaml" /drinks: post: x-speakeasy-entity-operation: Drink#create requestBody: required: true content: application/json: schema: $ref: "#/components/schemas/Drink" responses: "200": content: application/json: schema: $ref: "#/components/schemas/Drink" /drinks/{id}: parameters: - name: id in: path required: true schema: type: string get: x-speakeasy-entity-operation: Drink#read responses: "200": content: application/json: schema: $ref: "#/components/schemas/Drink" post: x-speakeasy-entity-operation: Drink#update requestBody: required: true content: application/json: schema: $ref: "#/components/schemas/Drink" responses: "200": content: application/json: schema: $ref: "#/components/schemas/Drink" delete: x-speakeasy-entity-operation: Drink#delete responses: "202": description: OK components: schemas: Drink: x-speakeasy-entity: Drink type: object properties: name: description: The name of the drink. type: string examples: - Old Fashioned - Manhattan - Negroni type: $ref: "#/components/schemas/DrinkType" price: description: The price of one unit of the drink in US cents. type: number examples: - 1000 # $10.00 - 1200 # $12.00 - 1500 # $15.00 required: - name - price ``` ### Mapping API Operations to Resources Methods An OpenAPI specification tracks a large list of [`Operation` objects](https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.1.0.md#operationObject). For a Terraform Provider generated by Speakeasy, the key element is the [the `x-speakeasy-entity-operation` annotation](/docs/terraform/customize/entity-mapping). This annotation clarifies the purpose of each operation in terms of how it affects the associated remote entity. ```yaml filename="openapi.yaml" /drinks: post: x-speakeasy-entity-operation: Drink#create requestBody: required: true content: application/json: schema: $ref: "#/components/schemas/Drink" responses: "200": content: application/json: schema: $ref: "#/components/schemas/Drink" /drinks/{id}: parameters: - name: id in: path required: true schema: type: string get: x-speakeasy-entity-operation: Drink#read responses: "200": content: application/json: schema: $ref: "#/components/schemas/Drink" post: x-speakeasy-entity-operation: Drink#update requestBody: required: true content: application/json: schema: $ref: "#/components/schemas/Drink" responses: "200": content: application/json: schema: $ref: "#/components/schemas/Drink" delete: x-speakeasy-entity-operation: Drink#delete responses: "202": description: OK components: schemas: Drink: x-speakeasy-entity: Drink type: object properties: name: description: The name of the drink. type: string examples: - Old Fashioned - Manhattan - Negroni id: description: The ID of the drink readOnly: true type: string type: $ref: "#/components/schemas/DrinkType" price: description: The price of one unit of the drink in US cents. type: number examples: - 1000 # $10.00 - 1200 # $12.00 - 1500 # $15.00 required: - name - price ``` ## Managing Complex API Semantics with Speakeasy APIs can have unique semantics not fully described by OpenAPI specs. To address this we have: 1. **Inference Rules**: Automatically derive most API semantics from the OpenAPI spec, with the exception of the `x-speakeasy-entity-operation` annotation. 2. **OpenAPI Extensions**: For more complex cases, use extensions to provide detailed configurations. These extensions are documented in the [Terraform Extensions](/docs/terraform/extensions/) section of this documentation. 3. **Support**: Our engineering team continually updates inference rules and extensions to accommodate new API patterns. Read more in our [documentation here](/docs/create-terraform), or [view more examples here](/guides). # What is hoisting? Source: https://speakeasy.com/docs/terraform/guides/hoisting Hoisting is a technique used in API design to reorganize the structure of data in API requests and responses. Its main goal is to simplify the way data is presented by moving nested or deeply structured data to a higher level in the API's response or request body. This process makes the data easier to work with for developers by reducing complexity and aligning the structure more closely with how resources are conceptually understood. In essence, hoisting "flattens" data structures. For APIs, this means transforming responses or requests so that important information is more accessible and not buried within nested objects. This is particularly beneficial when dealing with APIs that serve complex data models, as it can make the data easier to parse and use without extensive traversal of nested objects. ## When should you use hoisting? Hoisting is usually applied in specific scenarios to improve the design and usability of APIs: - **Complex Nested Structures**: Employ hoisting when your API deals with complex, deeply nested data. It streamlines access to important information, reducing the need for deep navigation. - **Frequent Data Access**: Use hoisting for elements that are often accessed or critical to operations, making them more directly accessible. - **Data Model Alignment**: Apply hoisting to better align the API's data structure with the conceptual model of the resources. ## Initial Structure: Without Hoisting Initially, our data structure represents a hierarchical model with nested entities, depicted as a tree. `x-speakeasy-entity: 1` at the top, with entities 2 and 3 as direct descendants. Entity 2 further nests entities 4, which branches into 5 and 6. {/* prettier-ignore */} ```bash (1) / \ 2 3 \ 4 / \ 5 6 ``` ## Step 1: Selecting an entity for hoisting Entity (2) is marked with `x-speakeasy-entity` for hoisting. {/* prettier-ignore */} ```bash 1 / \ (2) 3 \ 4 / \ 5 6 ``` ## Step 2 After applying hoisting, the structure is reorganized to prioritize `x-speakeasy-entity: 2`, making its leaf nodes directly accessible and flattening the remaining structure. `x-speakeasy-entity: 2` {/* prettier-ignore */} ```bash x-speakeasy-entity: 2 (2) / \ 3 4 / \ 5 6 ``` ## Real-World Application: Flattening a "data" property The JSON Schemas for `Drink`, `Drink`, and `{ drinkType: $DrinkType }` will be each considered the root of a Terraform Type Schema, and will be merged together to form the final Terraform Type Schema using Attribute Inference. However, this is not always desired. Consider this alternative response: ## Original In the original API design, the request and response body are structured equivalently, without nested elements. This approach is straightforward but might not always suit complex data relationships or requirements. ```yaml filename="openapi.yaml" /drinks: post: x-speakeasy-entity-operation: Drink#create parameters: - name: type in: query description: The type of drink required: false deprecated: true schema: $ref: "#/components/schemas/DrinkType" requestBody: required: true content: application/json: schema: $ref: "#/components/schemas/Drink" responses: "200": content: application/json: schema: $ref: "#/components/schemas/Drink" "5XX": $ref: "#/components/responses/APIError" default: $ref: "#/components/responses/UnknownError" ``` ## Alternate The alternate approach introduces a nested `data` property in the API response, which can encapsulate the drink information more distinctly, albeit adding a layer of complexity in data access. ```yaml filename="openapi.yaml" /drinks: post: x-speakeasy-entity-operation: Drink#create parameters: - name: type in: query description: The type of drink required: false deprecated: true schema: $ref: "#/components/schemas/DrinkType" requestBody: required: true content: application/json: schema: $ref: "#/components/schemas/Drink" responses: "200": content: application/json: schema: type: object properties: data: $ref: "#/components/schemas/Drink" "5XX": $ref: "#/components/responses/APIError" default: $ref: "#/components/responses/UnknownError" ``` ## Original Code Using the same request/response bodies, speakeasy would generate the following type schema ```go filename="drink_resource.go" func (r *DrinkResource) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) { resp.Schema = schema.Schema{ MarkdownDescription: "Drink Resource", Attributes: map[string]schema.Attribute{ "drink_type": schema.StringAttribute{ Optional: true, Computed: true Description: `The type of drink.`, stringvalidator.OneOf( "cocktail", "non-alcoholic", "beer", "wine", "spirit", "other", ), }, "name": schema.Int64Attribute{ Required: true, Description: `The name of the drink.`, }, "price": schema.StringAttribute{ Required: true, Description: `The price of one unit of the drink in US cents.`, }, }, } } ``` ## Alternative Code When generated, the provider schema would look like this: not understanding that `data` was a nested object, and instead treating it as a root of the schema. ```go filename="drink_resource.go" func (r *DrinkResource) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) { resp.Schema = schema.Schema{ MarkdownDescription: "Drink Resource", Attributes: map[string]schema.Attribute{ "drink_type": schema.StringAttribute{ Optional: true, Description: `The type of drink.`, stringvalidator.OneOf( "cocktail", "non-alcoholic", "beer", "wine", "spirit", "other", ), }, "name": schema.Int64Attribute{ Required: true, Description: `The name of the drink.`, }, "price": schema.StringAttribute{ Required: true, Description: `The price of one unit of the drink in US cents.`, }, "data": schema.SingleNestedAttribute{ Computed: true, Attributes: map[string]schema.Attribute{ "drink_type": schema.StringAttribute{ Computed: true Description: `The type of drink.`, }, "name": schema.Int64Attribute{ Computed: true Description: `The name of the drink.`, }, "price": schema.StringAttribute{ Computed: true, Description: `The price of one unit of the drink in US cents.`, }, }, }, }, } } ``` ## The Fix: Implementing Hoisting By applying the [`x-speakeasy-entity` annotation](/docs/terraform/customize/entity-mapping), we direct the schema generation process to consider the `Drink` schema as the root of the type schema, effectively flattening the response structure for easier access. ```yaml filename="openapi.yaml" /drinks: post: x-speakeasy-entity-operation: Drink#create parameters: - name: type in: query description: The type of drink required: false deprecated: true schema: $ref: "#/components/schemas/DrinkType" requestBody: required: true content: application/json: schema: $ref: "#/components/schemas/Drink" responses: "200": content: application/json: schema: type: object properties: data: $ref: "#/components/schemas/Drink" "5XX": $ref: "#/components/responses/APIError" default: $ref: "#/components/responses/UnknownError" components: schemas: Drink: x-speakeasy-entity: Drink type: object properties: name: description: The name of the drink. type: string examples: - Old Fashioned - Manhattan - Negroni type: $ref: "#/components/schemas/DrinkType" price: description: The price of one unit of the drink in US cents. type: number examples: - 1000 # $10.00 - 1200 # $12.00 - 1500 # $15.00 required: - name - price ``` ## Finalized Schema: Simplified Access The final provider schema reflects a flattened structure, similar to the original API design but with the flexibility to include nested data when necessary. ```go filename="drink_resource.go" func (r *DrinkResource) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) { resp.Schema = schema.Schema{ MarkdownDescription: "Drink Resource", Attributes: map[string]schema.Attribute{ "drink_type": schema.StringAttribute{ Optional: true, Computed: true Description: `The type of drink.`, stringvalidator.OneOf( "cocktail", "non-alcoholic", "beer", "wine", "spirit", "other", ), }, "name": schema.Int64Attribute{ Required: true, Description: `The name of the drink.`, }, "price": schema.StringAttribute{ Required: true, Description: `The price of one unit of the drink in US cents.`, }, }, } } ``` # Terraform Guides Source: https://speakeasy.com/docs/terraform/guides import { CardGrid } from "@/mdx/components"; import { terraformGuidesData } from "@/lib/data/docs/terraform-guides"; # Creating a Merged Terraform Entity Source: https://speakeasy.com/docs/terraform/guides/merged-entity Creating a merged Terraform entity involves combining data from separate API endpoints into a single Terraform resource. This process allows Terraform to manage complex entities that span multiple API calls for their lifecycle operations—create, read, update, and delete. ## Example Scenario: Merging Resource Entities Consider a scenario where managing a `drink` resource requires setting a `visibility` attribute post-creation using separate API endpoints: 1. **Create the drink**: Invoke `POST /drink` to create the drink entity. 2. **Set visibility**: Follow with `POST /drink/{id}/visibility` to configure visibility. ## Step 1: Define API Endpoints Identify the API endpoints involved in the operation. For instance, creating a `drink` and setting its visibility involves two distinct endpoints: ```yaml filename="openapi.yaml" /drinks: post: requestBody: required: true content: application/json: schema: type: object properties: name: type: string required: [name] responses: "200": content: application/json: schema: type: object properties: data: type: object properties: id: type: string required: [id] required: [data] /drink/{id}/visibility: post: requestBody: required: true content: application/json: schema: type: object properties: visibility: type: string enum: - public - private responses: "202": description: OK ``` ## Step 2: Annotate for Execution Order Mark both operations with annotations. For the operation requiring the `id` parameter, assign an `order` property value greater than the first operation to reflect its dependency on the `id` attribute. ```yaml filename="openapi.yaml" /drinks: post: x-speakeasy-entity-operation: Drink#create requestBody: required: true content: application/json: schema: type: object properties: name: type: string required: [name] responses: "200": content: application/json: schema: type: object properties: data: type: object properties: id: type: string required: [id] required: [data] /drink/{id}/visibility: post: x-speakeasy-entity-operation: Drink#create#2 requestBody: required: true content: application/json: schema: type: object properties: visibility: type: string enum: - public - private responses: "202": description: OK ``` ## Step 3: Configure Hoisting for Response Unwrapping Use `x-speakeasy-entity` annotations to simplify response handling by hoisting, avoiding nested data wrapping. ```yaml filename="openapi.yaml" /drinks: post: x-speakeasy-entity-operation: Drink#create requestBody: required: true content: application/json: schema: x-speakeasy-entity: Drink type: object properties: name: type: string required: [name] responses: "200": content: application/json: schema: type: object properties: data: type: object x-speakeasy-entity: Drink properties: id: type: string required: [id] required: [data] /drink/{id}/visibility: post: x-speakeasy-entity-operation: Drink#create#2 requestBody: required: true content: application/json: schema: type: object x-speakeasy-entity: Drink properties: visibility: type: string enum: - public - private responses: "202": description: OK ``` ## Advanced Example Step-by-Step When an [x-speakeasy-entity-operation](/docs/terraform/customize/entity-mapping) is defined, the request body, parameters, and response bodies of `CREATE` and `READ` operations are considered the root of the Terraform Type Schema. ## Step 1: Adding a `x-speakeasy-entity-operation: Drink#create` annotation marks the `POST /drinks` operation as CREATING a `drink` resource. ```yaml filename="openapi.yaml" /drinks: post: x-speakeasy-entity-operation: Drink#create parameters: - name: type in: query description: The type of drink required: false deprecated: true schema: $ref: "#/components/schemas/DrinkType" requestBody: required: true content: application/json: schema: $ref: "#/components/schemas/Drink" responses: "200": content: application/json: schema: $ref: "#/components/schemas/Drink" "5XX": $ref: "#/components/responses/APIError" default: $ref: "#/components/responses/UnknownError" components: schemas: Drink: type: object properties: name: description: The name of the drink. type: string examples: - Old Fashioned - Manhattan - Negroni type: $ref: "#/components/schemas/DrinkType" price: description: The price of one unit of the drink in US cents. type: number examples: - 1000 # $10.00 - 1200 # $12.00 - 1500 # $15.00 required: - name - price DrinkType: description: The type of drink. type: string enum: - cocktail - non-alcoholic - beer - wine - spirit - other ``` ## Step 2: Parameters, Request Bodies, and Response Bodies (associated with a 2XX status code) are each considered roots of the Terraform Type Schema ```yaml filename="openapi.yaml" /drinks: post: x-speakeasy-entity-operation: Drink#create parameters: - name: type in: query description: The type of drink required: false deprecated: true schema: $ref: "#/components/schemas/DrinkType" requestBody: required: true content: application/json: schema: $ref: "#/components/schemas/Drink" responses: "200": content: application/json: schema: $ref: "#/components/schemas/Drink" "5XX": $ref: "#/components/responses/APIError" default: $ref: "#/components/responses/UnknownError" components: schemas: Drink: type: object properties: name: description: The name of the drink. type: string examples: - Old Fashioned - Manhattan - Negroni type: $ref: "#/components/schemas/DrinkType" price: description: The price of one unit of the drink in US cents. type: number examples: - 1000 # $10.00 - 1200 # $12.00 - 1500 # $15.00 required: - name - price DrinkType: description: The type of drink. type: string enum: - cocktail - non-alcoholic - beer - wine - spirit - other ``` ## Step 3 The Terraform Type Schema merges all 3 of these together, and inferring links between the Operation and the Attributes. Note that similarly named attributes are merged together, and that the `DrinkType` attribute is inferred to be a `DrinkType` enum, rather than a `string`. ```yaml filename="derived.yaml" - create.parameters: type: from: "paths["/drinks"].post.parameters[0]" type: enum enumValues: type: string values: - cocktail - non-alcoholic - beer - wine - spirit - other - create.requestBody: name: from: "components.schemas.Drink.properties.name" description: The name of the drink. type: string examples: - Old Fashioned - Manhattan - Negroni drinkType: from: "components.schemas.Drink.properties.type" type: string enum: - cocktail - non-alcoholic - beer - wine - spirit - other price: from: "components.schemas.Drink.properties.price]" type: number - create.successResponseBody: name: from: "components.schemas.Drink.properties.name" description: The name of the drink. type: string examples: - Old Fashioned - Manhattan - Negroni drinkType: from: "components.schemas.Drink.properties.type" type: string enum: - cocktail - non-alcoholic - beer - wine - spirit - other price: from: "components.schemas.Drink.properties.price]" type: number ``` ## Step 4 These attributes are then merged together. If any properties conflict in type, an error is raised. ```yaml filename="derived.yaml" - create.requestShard: type: from: "paths["/drinks"].post.parameters[0]" type: enum enumValues: type: string values: - cocktail - non-alcoholic - beer - wine - spirit - other name: from: "components.schemas.Drink.properties.name" description: The name of the drink. type: string examples: - Old Fashioned - Manhattan - Negroni drinkType: from: "components.schemas.Drink.properties.type" type: string enum: - cocktail - non-alcoholic - beer - wine - spirit - other price: from: "components.schemas.Drink.properties.price]" type: number - create.responseShard: name: from: "components.schemas.Drink.properties.name" description: The name of the drink. type: string examples: - Old Fashioned - Manhattan - Negroni drinkType: from: "components.schemas.Drink.properties.type" type: string enum: - cocktail - non-alcoholic - beer - wine - spirit - other price: from: "components.schemas.Drink.properties.price]" type: number ``` ## Step 5 First, the `type` is taken from the OpenAPI `type`. It is `Optional: true` because it is not `required: true` or `nullable: true`. The description is taken from the OpenAPI `description`. The `examples` will be used to generate an example for the documentation ```go filename="drink_resource.go" func (r *DrinkResource) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) { resp.Schema = schema.Schema{ MarkdownDescription: "Drink Resource", Attributes: map[string]schema.Attribute{ "type": schema.StringAttribute{ Optional: true, Description: `The type of drink.`, }, }, } } ``` ## Step 6 Second, the `drinkType` is taken from the request and response bodies. It's converted to snake case as `drink_type` to follow terraform best-practices. It is `Optional: true` because it was not a member of the OpenAPI `requiredProperties`. It is `Computed: true` because even if not defined, the API is defined to (optionally) return a value for it. A plan validator is added with each of the enum values. This will be used to validate the plan at plan-time. ```go filename="drink_resource.go" func (r *DrinkResource) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) { resp.Schema = schema.Schema{ MarkdownDescription: "Drink Resource", Attributes: map[string]schema.Attribute{ "type": schema.StringAttribute{ Optional: true, Description: `The type of drink.`, }, "drink_type": schema.StringAttribute{ Optional: true, Computed: true Description: `The type of drink.`, stringvalidator.OneOf( "cocktail", "non-alcoholic", "beer", "wine", "spirit", "other", ), }, }, } } ``` ## Step 7 The other parameters are also pulled in from the request body. Both of them are required, with their type being derived from the equivalent Terraform primitive to their OpenAPI type. ```go filename="drink_resource.go" func (r *DrinkResource) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) { resp.Schema = schema.Schema{ MarkdownDescription: "Drink Resource", Attributes: map[string]schema.Attribute{ "type": schema.StringAttribute{ Optional: true, Description: `The type of drink.`, }, "drink_type": schema.StringAttribute{ Optional: true, Computed: true Description: `The type of drink.`, stringvalidator.OneOf( "cocktail", "non-alcoholic", "beer", "wine", "spirit", "other", ), }, "name": schema.Int64Attribute{ Required: true, Description: `The name of the drink.`, }, "price": schema.StringAttribute{ Required: true, Description: `The price of one unit of the drink in US cents.`, }, }, } } ``` ## Step 8: Cleanup In this API `drinkType` and `type` appear to refer to the same thing. `type` comes from a query parameter, whereas `drinkType` comes from the response body. This kind of pattern can be found in more legacy APIs, where parameters have been moved around and renamed, but older versions of those attributes are left around for backwards capability reasons. To clean up, we have many options we can apply to the API to describe what we want to happen: 1. `x-speakeasy-ignore: true` could be applied to the query parameter. After this point, it won't be configurable, and will never be sent. 2. `x-speakeasy-match: drinkType` could be applied to the query parameter. This will cause it to always be sent in the request, the same as the `drink_type` property. 3. `x-speakeasy-name-override: type` could be applied to the `drinkType` property. This will rename it as `type`, and ensure both `drinkType` request body key and `type` query parameter are both always sent. ```go filename="drink_resource.go" func (r *DrinkResource) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) { resp.Schema = schema.Schema{ MarkdownDescription: "Drink Resource", Attributes: map[string]schema.Attribute{ "drink_type": schema.StringAttribute{ Optional: true, Computed: true Description: `The type of drink.`, stringvalidator.OneOf( "cocktail", "non-alcoholic", "beer", "wine", "spirit", "other", ), }, "name": schema.Int64Attribute{ Required: true, Description: `The name of the drink.`, }, "price": schema.StringAttribute{ Required: true, Description: `The price of one unit of the drink in US cents.`, }, }, } } ``` # Overlay to remove non-Terraform endpoints Source: https://speakeasy.com/docs/terraform/guides/remove-nontf-endpoints Often generating a Terraform provider from your OpenAPI spec means using a subset of endpoints to generate the provider. Endpoints or `paths` in your spec not used for terraform generation will still be used to generate a Go client inside your provider. This can lead to a bloated provider with unused endpoints. If you want to remove these consider using the following overlay. ```yaml overlay: 1.0.0 info: title: Overlay openapi.yaml version: 0.1.0 actions: - target: $.paths.*.*[?(!@.x-speakeasy-entity-operation && @.operationId)] # Find all paths that do not have x-speakeasy-entity-operation and operationId remove: true ``` When applied to an OpenAPI specification this overlay will remove any paths from the specification that are not tagged with `x-speakeasy-entity-operation`. The operation needed to map an OpenAPI `operationId` to a terraform resource. To apply this overlay run `speakeasy overlay apply -s {your-spec.yaml} -o {overlay.yaml}`. This will generate a new OpenAPI specification with the paths removed. Finally add this to your terraform generation workflow by using `speakeasy configure sources` to add an overlay to any source with an existing OpenAPI specification configured. # Terraform Registry Source: https://speakeasy.com/docs/terraform/publish-terraform The Speakeasy Generation GitHub Action can be configured to publish a generated Terraform provider. However, Terraform providers cannot be generated with Speakeasy while operating in monorepo mode. HashiCorp requires a separate repository for the Terraform provider code that follows their naming convention and must be public. For more information, refer to the [HashiCorp Terraform Registry documentation](https://developer.hashicorp.com/terraform/registry/providers/publishing#preparing-your-provider). Use the following steps to publish a generated Terraform provider to the HashiCorp Terraform Registry. 1. Create a repository for the Terraform provider: - Name the repository `terraform-provider-{NAME}`, with the `NAME` containing lowercase letters. - Ensure it is a public repository. 2. Sign your Terraform provider releases with a signing key. Create and export a GNU Privacy Guard (GPG) signing key following the instructions in the GitHub guide to [Generating a new GPG key](https://docs.github.com/en/authentication/managing-commit-signature-verification/generating-a-new-gpg-key). Generate your GPG key using either the Digital Signature Algorithm (DSA) or the Rivest–Shamir–Adleman (RSA) algorithm. 3. Take note of the following values: - The GPG private key. - The GPG passphrase. - The GPG public key. 4. Add the ASCII-armored public key to the Terraform repository. 5. Your GPG private key and GPG passphrase will be configured automatically when entered into the Speakeasy CLI. Ensure the following secrets are available to your repository: - The GPG private key, `terraform_gpg_secret_key`. - The GPG passphrase, `terraform_gpg_passphrase`. 6. The first time you create and publish a Terraform provider using the Speakeasy Generation GitHub Action, you need to manually add it to the Terraform Registry. Subsequent updates will be published automatically. To begin this process, follow the [Terraform Registry instructions](https://developer.hashicorp.com/terraform/registry/providers/publishing) and agree to the Terraform terms and conditions. Note that you will need to be an organizational admin to complete this step. 7. Add the following file to the `.github/workflows` directory to create releases for the Terraform provider using GoReleaser. If all the above steps are complete, the HashiCorp registry automatically picks up new changes. ```yaml filename="terraform_publish.yaml" # Terraform Provider release workflow. name: Release # This GitHub Action creates a release when a tag that matches the pattern # "v*" (e.g. v0.1.0) is created. on: push: tags: - 'v*' workflow_dispatch: # Releases need permission to read and write the repository contents. # GitHub considers creating releases and uploading assets as writing content. permissions: contents: write jobs: goreleaser: runs-on: ubuntu-latest steps: - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 with: # Allow GoReleaser to access older tag information. fetch-depth: 0 - uses: actions/setup-go@4d34df0c2316fe8122ab82dc22947d607c0c91f9 # v4.0.0 with: go-version-file: 'go.mod' cache: true - name: Import GPG key uses: crazy-max/ghaction-import-gpg@111c56156bcc6918c056dbef52164cfa583dc549 # v5.2.0 id: import_gpg with: gpg_private_key: ${{ secrets.terraform_gpg_secret_key }} passphrase: ${{ secrets.terraform_gpg_passphrase }} - name: Run GoReleaser uses: goreleaser/goreleaser-action@286f3b13b1b49da4ac219696163fb8c1c93e1200 # v6.0.0 with: args: release --clean env: # GitHub sets the GITHUB_TOKEN secret automatically. GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} GPG_FINGERPRINT: ${{ steps.import_gpg.outputs.fingerprint }} ``` 8. Finally, create a [GoReleaser](https://goreleaser.com/) file in the root of your repository: ```yaml filename=".goreleaser.yml" # Visit https://goreleaser.com for documentation on how to customize this # behavior. version: 2 before: hooks: # This is just an example and not a requirement for building or publishing providers. - go mod tidy builds: - env: # GoReleaser does not work with CGO and could also complicate # usage by users in CI/CD systems, like Terraform Cloud, where # users are unable to install libraries. - CGO_ENABLED=0 mod_timestamp: '{{ .CommitTimestamp }}' flags: - -trimpath ldflags: - '-s -w -X main.version={{.Version}} -X main.commit={{.Commit}}' goos: - freebsd - windows - linux - darwin goarch: - amd64 - '386' - arm - arm64 ignore: - goos: darwin goarch: '386' binary: '{{ .ProjectName }}_v{{ .Version }}' archives: - format: zip name_template: '{{ .ProjectName }}_{{ .Version }}_{{ .Os }}_{{ .Arch }}' checksum: extra_files: - glob: 'terraform-registry-manifest.json' name_template: '{{ .ProjectName }}_{{ .Version }}_manifest.json' name_template: '{{ .ProjectName }}_{{ .Version }}_SHA256SUMS' algorithm: sha256 signs: - artifacts: checksum args: # If you are using this in a GitHub Action or some other automated pipeline, you # need to pass the batch flag to indicate it's not interactive. - "--batch" - "--local-user" - "{{ .Env.GPG_FINGERPRINT }}" # set this environment variable for your signing key - "--output" - "${signature}" - "--detach-sign" - "${artifact}" release: extra_files: - glob: 'terraform-registry-manifest.json' name_template: '{{ .ProjectName }}_{{ .Version }}_manifest.json' # If you want to manually examine the release before it goes live, uncomment this line: # draft: true changelog: disable: true ``` Whenever you generate and merge a new PR for your Terraform provider, it will be automatically versioned and released to the HashiCorp Registry. # Using the CLI to populate code samples without GitHub Actions Source: https://speakeasy.com/guides/cli/code-samples-without-github-actions This guide explains how to use the Speakeasy CLI to generate and publish code samples for your API documentation when you're not using GitHub Actions. ## Overview Speakeasy automatically generates code samples for your OpenAPI document and makes them available via a public URL that can be integrated with documentation platforms like Scalar. While this process is typically automated with GitHub Actions, you can achieve the same result using just the Speakeasy CLI in your own custom workflow. ## Prerequisites To follow this guide, you need: - **The Speakeasy CLI:** Install the CLI on your computer. - **An OpenAPI document:** Speakeasy generates your SDKs based on your OpenAPI document - **A Speakeasy account and API key:** Ensure you have access to the Speakeasy workspace. - **A CI environment:** The generation must run from a CI environment or with the `CI_ENABLED` environment variable set to `true`. ## Generating code samples with the Speakeasy CLI Follow these steps to populate your OpenAPI document and make it available for use with documentation platforms: ### Configure your workflow First, set up your workflow configuration file: ```bash speakeasy configure ``` This creates a `.speakeasy/workflow.yaml` file that defines your SDK generation targets and code samples configuration. ### Generate SDKs and code samples Run the Speakeasy CLI to generate your SDKs and code samples: ```bash speakeasy run ``` This command: - Downloads or loads your OpenAPI document - Validates the OpenAPI document - Generates SDKs for your configured languages - Creates code samples for each operation in your API ### Promote code samples to main The critical step for enabling automated code samples is to tag both your source specification (OpenAPI document) and generated code samples with the `main` tag: ```bash speakeasy tag promote -s my-source-name -c my-target-name -t main ``` This command tags both the source OpenAPI document (`-s my-source-name`) and the generated code samples (`-c my-target-name`) as "official" so they can be incorporated into the public URL. Similar to how the `main` branch in GitHub represents the production-ready version of your code, the `main` tag in Speakeasy indicates these are the production-ready specifications and code samples that should be publicly available. Replace `my-source-name` and `my-target-name` with the names defined in your workflow configuration. ### Access the public URL Once you've tagged your code samples with `main`, Speakeasy automatically starts building a combined OpenAPI document in the background. The combined document is available at a public URL that you can use with documentation platforms like Scalar. To find this URL, open the Speakeasy workspace and navigate to the **Docs** tab. Click **Integrate with Docs Provider** to see the URL to the combined OpenAPI document with populated code samples. Currently, there's no CLI command to retrieve this URL programmatically. ### Integrate with docs providers Once you have the public URL, you can integrate it with various documentation providers. Speakeasy offers detailed integration guides for several popular docs platforms: - [Scalar](/docs/sdk-docs/integrations/scalar) is a modern API documentation platform. - [ReadMe](/docs/sdk-docs/integrations/readme) offers an interactive API explorer and documentation. - [Mintlify](/docs/sdk-docs/integrations/mintlify) provides developer documentation with an interactive playground. - [Bump.sh](/docs/sdk-docs/integrations/bump) hosts API documentation and catalogs. ## Automating the process Use the following steps to create a fully automated workflow without GitHub Actions: - Create a script or CI pipeline that runs `speakeasy run` to generate SDKs and code samples. - Add `speakeasy tag promote -s my-source-name -c my-target-name -t main` to tag both the source OpenAPI document and generated code samples. - The public URL automatically updates the OpenAPI document with the latest code samples. ### Why use CLI tagging instead of GitHub Actions? While GitHub Actions provides a convenient way to automate code sample generation and tagging, the CLI approach offers several advantages for teams: - **Platform independence:** Use any CI/CD system (such as Jenkins, GitLab CI, CircleCI, or Azure DevOps) instead of being limited to GitHub Actions. - **Custom workflows:** Integrate code sample generation into existing build processes or deployment pipelines. - **Local development:** Generate and test code samples locally before pushing changes. - **Private repositories:** Work with code that isn't hosted on GitHub or is hosted in private repositories with restricted access. - **Enterprise environments:** Provide support for organizations with specific security or compliance requirements that prevent them from using GitHub Actions. ### Example workflow script Here's a simple bash script example that could be used in a custom CI pipeline: ```bash #!/bin/bash # Install Speakeasy CLI if needed # curl -fsSL https://raw.githubusercontent.com/speakeasy-api/speakeasy/main/install.sh | sh # Set CI environment variable if not running in CI export CI_ENABLED=true # Generate SDKs and code samples speakeasy run # Tag both source specification and code samples as main speakeasy tag promote -s my-source-name -c my-python-target -t main speakeasy tag promote -s my-source-name -c my-typescript-target -t main echo "Code samples have been generated and tagged. They will be available at the public URL in the Speakeasy dashboard." ``` # Authenticating with local environment variables Source: https://speakeasy.com/guides/hooks/env-auth-hook When authenticating with an API using a SDK, its a common pattern for the value of an `API_KEY` or `token` to default to the value of an environment variable. This allows you to easily switch between different environments without changing the code. In this example, we'll show you how to use a [SDK Hook](/docs/customize/code/sdk-hooks) enable your users to authenticate with your API using local environment variables. A SDK Hook is a function that will be executed by the SDK at a specific point in the request lifecycle. For this use case we'll leverage a `BeforeRequest` hook. Inside of our Speakeasy generated SDK hooks are written in the `src/hooks/` directory. We'll make a new hook called in a file called `auth.ts`. ```typescript filename="src/hooks/auth.ts" import { BeforeRequestHook } from "./types"; export const injectAPIKey: BeforeRequestHook = { beforeRequest: async (_, request) => { const authz = request.headers.get("Authorization"); if (authz) { return request; } let token = ""; if (typeof process !== "undefined") { token = process.env["API_KEY"] ?? ""; } if (!token) { throw new Error("The API_KEY environment variable is missing or empty; either provide it"); } request.headers.set("Authorization", `Bearer ${token}`); return request; }, }; ``` This hook will check for the presence of an environment variable named `API_KEY` and if it exists, it will add it to the `Authorization` header of the request. Finally to ensure the SDK uses this hook, we need to add it to make sure it is registered with the SDK. This is done in the `src/hooks/registration.ts` file. ```typescript filename="src/hooks/registration.ts" import { injectAPIKey } from "./auth"; import { Hooks } from "./types"; /* * This file is only ever generated once on the first generation and then is free to be modified. * Any hooks you wish to add should be registered in the initHooks function. Feel free to define them * in this file or in separate files in the hooks folder. */ export function initHooks(hooks: Hooks) { // Add hooks by calling hooks.register{ClientInit/BeforeRequest/AfterSuccess/AfterError}Hook // with an instance of a hook that implements that specific Hook interface // Hooks are registered per SDK instance, and are valid for the lifetime of the SDK instance hooks.registerBeforeRequestHook(injectAPIKey); } ``` Finally make sure to update the usage snippet in your readme to reference the environment variable. # Add telemetry to your SDK with SDK hooks and Posthog Source: https://speakeasy.com/guides/hooks/posthog-telemetry-hook ## Prerequisites - You will need a Posthog account (If you don't have one, you can sign up [here](https://posthog.com/signup)) ## Overview This guide will walk you through adding telemetry to a TypeScript SDK using SDK hooks and the Posthog Node SDK. SDK hooks are a way to inject custom actions at various points in the SDK's execution. You can inject custom actions at the following points in the SDK's execution: - `On SDK Initialization` - `Before a request is executed` - `After a successful response` - `After an error response` ## Adding the Posthog SDK to your project To add the Posthog SDK to your project, you will need to add the dependency to your Speakeasy SDK's `gen.yaml` file under the dependancies section: ```yaml configVersion: 2.0.0 generation: sdkClassName: Petstore ... typescript: version: 0.7.11 additionalDependencies: dependencies: posthog-node: ^4.0.1 <- This is the line you need to add, ensure the version you add adheres to NPM package standards. ``` After adding the dependency, the Posthog SDK will be included in your projects package.json file every time you generate your SDK. ## Adding your first SDK hook Now that you have the Posthog SDK included in your project, you can start adding SDK hooks to your SDK. First, create a new file in the `src/hooks` directory, and name it `telemetry_hooks.ts`. In this file you will need to import the hook types for each hook you want to use, as well as the PostHog SDK and initialize the PostHog SDK with your API Key. I will be using all four of the hooks in this guide, but you can choose which hooks you want to use. ```typescript import { PostHog } from "posthog-node"; import { AfterErrorContext, AfterErrorHook, AfterSuccessContext, AfterSuccessHook, BeforeRequestContext, BeforeRequestHook, SDKInitHook, SDKInitOptions, } from "./types"; const PostHogClient = new PostHog("phc_xxxxxxxxxxxxxxxxxx", { host: "https://us.i.posthog.com", }); ``` Next you can create a class that will hold your hooks. Our class will be `TelemetryHooks` and our first hook will be an `On SDK Initialization` hook. Below is the start of our `TelemetryHooks` class. This class will hold all of our telemetry hooks. ```typescript export class TelemetryHooks implements SDKInitHook { sdkInit(opts: SDKInitOptions): SDKInitOptions { const { baseURL, client } = opts; return { baseURL, client }; } } ``` This hook allows us to inject custom actions and capture an `SDK Init` event to Posthog at the time the SDK is initialized. Here are the key points to the capture method outlined below: - **distinctId**: A `distinctId` can be provided, serving as a unique identifier for either a user or a session. This is particularly useful for tracking recurring events across different sessions, which can aid in identifying and troubleshooting issues. - **event**: The name of the event is specified to facilitate easier sorting and analysis within Posthog. - **properties**: An arbitrary set of properties; extra information relevant to the event. Here the contents of the `opts` parameter are added to the event as properties. This allows for detailed tracking of the initialization parameters. Lastly Posthog's SDKs are asynchronous, so we need to shutdown the SDK after we are done, ensuring events are flushed out before the process ends. ```typescript export class TelemetryHooks implements SDKInitHook { sdkInit(opts: SDKInitOptions): SDKInitOptions { const { baseURL, client } = opts; PostHogClient.capture({ distinctId: "distinct_id_of_the_user", event: "SDK Init", properties: { baseURL, client, }, }); PostHogClient.shutdown(); return { baseURL, client }; } } ``` Now that we have our `TelemetryHooks` class, we can add the remainder of the hooks. ```typescript export class TelemetryHooks implements SDKInitHook, BeforeRequestHook, AfterSuccessHook, AfterErrorHook {// <- add in the remainder of the hooks you will be implementing ... } ``` The structure of the remaining hooks is the same, We just supply a `distinctId`, an `event`, and `properties` for each hook. ```typescript async beforeRequest( hookCtx: BeforeRequestContext, request: Request ): Promise { PostHogClient.capture({ distinctId: "distinct_id_of_the_user", event: "Before Request", properties: { hookCtx: hookCtx, }, }); await PostHogClient.shutdown(); return request; } async afterSuccess( hookCtx: AfterSuccessContext, response: Response ): Promise { PostHogClient.capture({ distinctId: "distinct_id_of_the_user", event: "After Success", properties: { hookCtx: hookCtx, response: response, }, }); await PostHogClient.shutdown(); return response; } async afterError( hookCtx: AfterErrorContext, response: Response | null, error: unknown ): Promise<{ response: Response | null; error: unknown }> { PostHogClient.capture({ distinctId: "distinct_id_of_the_user", event: "After Error", properties: { hookCtx: hookCtx, response: response, error: error, }, }); await PostHogClient.shutdown(); return { response, error }; } ``` Once all the hooks are implemented, you can now use the `TelemetryHooks` class in your SDK. in the `src/hooks/registration.ts`, you simply need to import the class from the file you created the hooks in, and register them following the directions in the comment. ```typescript import { TelemetryHooks } from "./telemetry_hooks"; import { Hooks } from "./types"; /* * This file is only ever generated once on the first generation and then is free to be modified. * Any hooks you wish to add should be registered in the initHooks function. Feel free to define them * in this file or in separate files in the hooks folder. */ export function initHooks(hooks: Hooks) { // Add hooks by calling hooks.register{ClientInit/BeforeCreateRequest/BeforeRequest/AfterSuccess/AfterError}Hook // with an instance of a hook that implements that specific Hook interface // Hooks are registered per SDK instance, and are valid for the lifetime of the SDK instance hooks.registerBeforeRequestHook(new TelemetryHooks()); hooks.registerAfterSuccessHook(new TelemetryHooks()); hooks.registerAfterErrorHook(new TelemetryHooks()); hooks.registerSDKInitHook(new TelemetryHooks()); } ``` You can now regenerate your SDK and use the new hooks. Running API calls using this SDK will surface events up to Posthog. And you can review all the details in Posthog. ![Screenshot of event data in Posthog.](/assets/guides/posthog-event-data.png) You can review all the code outlined in this guide in the [SDK Hooks](https://github.com/speakeasy-api/examples/blob/main/posthog-hook-ts/src/hooks/posthog.ts) repository. # Capture errors with SDK hooks and Sentry Source: https://speakeasy.com/guides/hooks/sentry-error-hook ## Prerequisites You will need: - A Sentry account [(If you don't have one, you can sign up [here](https://sentry.io/signup))] - A Sentry project ## Overview This guide will show you how to use SDK hooks to integrate error collection into a TypeScript SDK with the Sentry Node SDK, allowing you to insert custom actions at various stages of the SDK's execution: - `On SDK Initialization` - `Before a request is executed` - `After a successful response` - `After an error response` ## Adding the Sentry SDK to your project To add the Sentry SDK to your project, you will need to add the dependency to your Speakeasy SDK's `gen.yaml` file under the dependancies section: ```yaml configVersion: 2.0.0 generation: sdkClassName: Petstore ... typescript: version: 0.8.4 additionalDependencies: dependencies: '@sentry/node': ^8.9.2 # <- This is the line you need to add, ensure the version you add adheres to NPM package standards. ``` After adding the dependency, the Sentry SDK will be included in your projects package.json file every time you generate your SDK. ## Adding your first SDK hook With the Sentry SDK included in your project, you can start adding SDK hooks. 1. Create a new file: In the `src/hooks` directory, create a file named `error_hooks.ts`. 2. Import hook types and Sentry SDK: In this file, import the necessary hook types and initialize the Sentry SDK with your project's DSN. ```typescript import * as Sentry from "@sentry/node"; import { AfterErrorContext, AfterErrorHook } from "./types"; Sentry.init({ dsn: process.env.SENTRY_DSN, // <- This is your Sentry DSN, you can find this in your Sentry project settings. }); ``` Next create an `ErrorHooks` class to hold your hooks. ```typescript export class ErrorHooks implements AfterErrorHook { afterError( hookCtx: AfterErrorContext, response: Response | null, error: unknown, ): { response: Response | null; error: unknown } { return { response, error }; } } ``` This hook allows us to inject custom code that runs on error responses and capture an `SDK Error` event to Sentry at the time the error occurs. Here are the key points to the class outlined below: - Use the `AfterErrorHook` interface to define the type of the hook. - Capture the `hookCtx`, `response`, and `error` in the `afterError` method. - Return the `response` and `error` so that the SDK can continue to process the error. Specific notes for using Sentry here: - Add a breadcrumb to Sentry to capture additional details regarding the error. - capturing the error in Sentry using `Sentry.captureException(error)`. ```typescript export class ErrorHooks implements AfterErrorHook { afterError( hookCtx: AfterErrorContext, response: Response | null, error: unknown, ): { response: Response | null; error: unknown } { Sentry.addBreadcrumb({ category: "sdk error", message: "An error occurred in the SDK", level: "error", data: { hookCtx, response, error, }, }); Sentry.captureException(error); return { response, error }; } } ``` Once this hook is implemented, you can now use the `ErrorHooks` class in your SDK. In the `src/hooks/registration.ts` file import and register the class from the file you created the hooks in: ```typescript import { ErrorHooks } from "./error_hooks"; import { Hooks } from "./types"; /* * This file is only ever generated once on the first generation and then is free to be modified. * Any hooks you wish to add should be registered in the initHooks function. Feel free to define them * in this file or in separate files in the hooks folder. */ export function initHooks(hooks: Hooks) { // Add hooks by calling hooks.register{ClientInit/BeforeCreateRequest/BeforeRequest/AfterSuccess/AfterError}Hook // with an instance of a hook that implements that specific Hook interface // Hooks are registered per SDK instance, and are valid for the lifetime of the SDK instance hooks.registerAfterErrorHook(new ErrorHooks()); } ``` You can now regenerate your SDK and use the new hooks. Running API calls with this SDK will send error events to your Sentry project, where you can review all the details for each error. ![Screenshot of error data in Sentry.](/assets/guides/sentry-error-data.png) Review all the code outlined in this guide by visiting the [SDK Hooks](https://github.com/speakeasy-api/examples/blob/main/sentry-hook-ts/src/hooks/sentry.ts) repository. # Setting user agents in browser environments Source: https://speakeasy.com/guides/hooks/user-agent-hook ## Overview When using Speakeasy SDKs in browser environments, setting the user agent header directly is restricted by browser security policies. This guide demonstrates how to use SDK hooks to set user agent information in a browser-compatible way. ## Understanding the challenge Browsers prevent direct modification of the `User-Agent` header for security reasons. When attempting to set this header in browser environments: - The header modification may silently fail - No error will be thrown - The original browser user agent will be used instead ## Solution using SDK hooks We can use a `BeforeRequestHook` to implement a fallback mechanism that ensures our SDK version information is properly transmitted, even in browser environments. ```typescript import { BeforeRequestContext, BeforeRequestHook, Awaitable } from "./types"; import { SDK_METADATA } from "../lib/config"; export class CustomUserAgentHook implements BeforeRequestHook { beforeRequest(_: BeforeRequestContext, request: Request): Awaitable { const version = SDK_METADATA.sdkVersion; const ua = `speakeasy-sdk/${version}`; // Try to set the standard user-agent header first request.headers.set("user-agent", ua); // Check if the header was actually set (it may silently fail in browsers) if (!request.headers.get("user-agent")) { // Fall back to a custom header if the user-agent couldn't be set request.headers.set("x-sdk-user-agent", ua); } return request; } } ``` ## How the hook works 1. The hook attempts to set the standard `user-agent` header first 2. It then checks if the header was successfully set 3. If the header wasn't set (which happens in browsers), it falls back to using a custom header `x-sdk-user-agent` 4. This ensures your SDK version information is always transmitted, regardless of browser restrictions ## Adding the hook to your SDK Register the hook in your SDK's hook registration file: ```typescript import { CustomUserAgentHook } from "./user_agent"; import { Hooks } from "./types"; export function initHooks(hooks: Hooks) { hooks.registerBeforeRequestHook(new CustomUserAgentHook()); } ``` ## Using hooks with generated SDKs When working with SDKs generated by Speakeasy, you'll want to follow these best practices: - Always implement the fallback mechanism to handle browser environments - Use a consistent format for your user agent string (e.g., `sdk-name/version`) - Consider including additional relevant information in the user agent string - Test the implementation in both browser and non-browser environments This approach ensures your Speakeasy SDK can properly identify itself to your API while remaining compatible with browser security restrictions. # Speakeasy guides Source: https://speakeasy.com/guides Here, you'll find a comprehensive collection of step-by-step guides, best practices, and tutorials to help you get the most out of Speakeasy. Whether you're just getting started or looking to delve deeper into advanced features, our guides are tailored to support you at every step of your journey. import { CardGrid } from "@/components/card-grid"; import { allFrameworkGuidesData } from "@/lib/data/openapi/framework-all-guides"; import { guideCategories } from "@/lib/data/docs/guide-categories"; import { openapiGuidesData } from "@/lib/data/docs/openapi-guides"; import { sdkGuidesData } from "@/lib/data/docs/sdk-guides"; import { overlayGuidesData } from "@/lib/data/docs/overlay-guides"; import { hookGuidesData } from "@/lib/data/docs/hook-guides"; import { terraformGuidesData } from "@/lib/data/docs/terraform-guides"; ## Guide Categories ## Framework Guides If you haven't used OpenAPI before, start with our guide to creating an OpenAPI schema from your existing code: ## OpenAPI Guides ## SDK Guides ## SDK Overlays ## Hook Guides ## Terraform Guides # Using Path Fragments to Solve Equivalent Path Signatures Source: https://speakeasy.com/guides/openapi/path-fragments Equivalent path signatures in an OpenAPI specification can cause validation errors and/or unexpected behaviors. Adding unique path fragments is an effective method for resolving these conflicts. Path fragments appended with an anchor (e.g., `#id`, `#name`) make paths unique without altering the API's functionality since the anchor section is not sent in API requests. ## When to Use Path Fragments Path fragments should be used when the OpenAPI specification contains multiple paths with equivalent signatures but distinct parameter names. For example: ```yaml paths: /v13/deployments/{idOrUrl}: get: summary: "Get deployment by ID or URL" parameters: - name: idOrUrl in: path required: true schema: type: string /v13/deployments/{id}: get: summary: "Get deployment by ID" parameters: - name: id in: path required: true schema: type: string ``` These paths conflict because their structures are identical, even though their parameter names differ. ## Add Path Fragments Modify the conflicting paths by appending a unique anchor fragment to each. For example: ```yaml paths: /v13/deployments/{idOrUrl}#idOrUrl: get: summary: "Get deployment by ID or URL" parameters: - name: idOrUrl in: path required: true schema: type: string /v13/deployments/{id}#id: get: summary: "Get deployment by ID" parameters: - name: id in: path required: true schema: type: string ``` Corresponding SDK method signatures: ```typescript // For /v13/deployments/{idOrUrl}#idOrUrl function getDeploymentByIdOrUrl(idOrUrl: string): Deployment { // Implementation } // For /v13/deployments/{id}#id function getDeploymentById(id: string): Deployment { // Implementation } ``` ## Considerations - **Impact on Tooling**: Most tools will handle path fragments correctly, but confirm that all downstream tooling supports the updated specification. - **Path Fragments in Overlays**: Path fragments cannot be added using overlays. They must be introduced directly in the upstream OpenAPI specification. # What is the code samples extension? Source: https://speakeasy.com/guides/openapi/x-codesamples import { Table } from "@/mdx/components"; Many API documentation providers provide code snippets in multiple languages to help developers understand how to use the API. However, these snippets may not correspond to a usage snippet from an existing SDK provided by the API, which reduces the value of the API documentation and can lead to inconsistent integrations, depending on whether a user discovers the API docs or the SDK first. The `x-codeSamples` (previously called `x-code-samples`) extension is a widely accepted spec extension that enables the addition of custom code samples in one or more languages to operation IDs in your OpenAPI specification. When custom code samples are added using the code samples extension, documentation providers will render the usage snippet in the right-hand panel of the documentation page: ![Screenshot of Inkeep's API docs showing featured SDK usage.](/assets/guides/docs-example.png) ## Anatomy of the extension
Documentation providers that support `x-codeSamples` include but are not limited to: - Mintlify - Readme - Redocly - Stoplight ## Example usage Here is a basic example of using the `x-codeSamples` extension with a `curl` snippet. ```yaml openapi: "3.0.3" info: ... tags: [...] paths: /example: get: summary: Example summary description: Example description operationId: examplePath responses: [...] parameters: [...] x-codeSamples: - lang: "cURL" label: "CLI" source: | curl --request POST \ --url 'https://data.apiexample.com/api/example/batch_query/json?format=json' \ --header 'content-type: application/octet-stream: ' \ --data '{}' ``` Now let's extend this to a more complex example: a TypeScript SDK for an LLM chat API. ```yaml openapi: "3.0.3" info: ... tags: [...] paths: /chat_sessions/chat_results: post: summary: Create Chat Session operationId: create tags: [chat_session] requestBody: content: application/json: schema: $ref: "#/components/schemas/CreateChatSession" required: true x-codeSamples: - lang: "typescript" label: "create_chat_session" source: | import { ChatSDK } from "@llm/chat-sdk"; async function run() { const sdk = new ChatSDK({ apiKey: "", }); const res = await sdk.chatSession.create({ integrationId: "", chatSession: { messages: [ { role: "user", content: "How do I get started?", }, ], }, stream: true, }); /* Example of handling a streamed response */ if (res.chatResultStream == null) { throw new Error("failed to create stream: received null value"); } let chatSessionId: string | undefined | null = undefined; for await (const event of res.chatResultStream) { if (event.event == "message_chunk") { console.log("Partial message: " + event.data.contentChunk); chatSessionId = event.data.chatSessionId; } if (event.event == "records_cited") { console.log("Citations: ", JSON.stringify(event.data.citations, null, 2)); } } } run(); ``` Multiple code samples can be added to a single `operationId` to support examples in any number of languages by adding multiple keys under the `x-codeSamples` extension. ```yaml openapi: "3.0.3" info: ... tags: [...] paths: /chat: get: summary: Example summary description: Example description operationId: examplePath responses: [...] parameters: [...] x-codeSamples: - lang: "typescript" label: "chat_ts" source: | ..... ..... - lang: "python" label: "chat_python" source: | ..... ..... ``` ## Generating code samples To generate SDK code samples for your OpenAPI document, run the following command: ```bash speakeasy generate codeSamples -s {{your-spec.yaml}} --langs {{lang1}},{{lang2}} --out code-samples-overlay.yaml ``` This command creates an [overlay](/docs/prep-openapi/overlays/create-overlays) with code samples for every `operationId` in your OpenAPI document. To apply the overlay to your specification, run: ```bash speakeasy overlay apply -o code-samples-overlay.yaml -s {{your-spec.yaml}} -o {{output-spec.yaml}} ``` The final output spec will include `codeSamples` inline. ## Adding code sample generation to your workflow To include `codeSamples` overlay generation in your Speakeasy workflow, add the following to your `.speakeasy/workflow.yaml` for any `target` you have configured: ```yaml filename=".speakeasy/workflow.yaml" targets: my-target: target: typescript source: my-source codeSamples: output: codeSamples.yaml ``` If you want the overlay to be automatically applied on the source, create another workflow entry using `speakeasy configure` as follows: ![Configure Sources 1](/assets/docs/spec-workflow/1.png) Then add the overlay created by Speakeasy to inject code snippets into your spec: ![Configure Sources 2](/assets/docs/spec-workflow/2.png) Finally, provide the name and path for your output OpenAPI spec. This will be the final spec used by Mintlify. ![Configure Sources 3](/assets/docs/spec-workflow/3.png) # What is a Monorepo? Source: https://speakeasy.com/guides/sdks/creating-a-monorepo import { Callout } from "@/mdx/components"; This section outlines an advanced setup process that may not be suitable for all users. For most use cases, we recommend adopting the simpler approach of a single SDK per GitHub repository. To setup a single SDK per Github see our [SDK quickstart](/docs/sdks/create-client-sdks). A monorepo is a unified repository containing multiple SDKs, each corresponding to a unique OpenAPI specification. This approach offers a centralized location for discovering available SDKs while allowing for the independent download and management of each SDK as needed. Each SDK resides in its own subfolder, and can be made complete with individual GitHub workflows for regeneration and release processes. ## Repository Structure The monorepo's structure is designed for scalability and easy navigation. In our example, we will discuss two SDKs: ServiceA and ServiceB, each found in their respective subfolders. The typical structure of such a monorepo is as follows: ```yaml .github/workflows/ - serviceA_generate.yaml # Github Workflow for generating the ServiceA SDK, generated by speakeasy cli - serviceA_release.yaml # Github Workflow for releasing and publishing the ServiceA SDK, generated by speakeasy cli - serviceB_generate.yaml # Github Workflow for generating the ServiceB SDK, generated by speakeasy cli - serviceB_release.yaml # Github Workflow for releasing and publishing the ServiceB SDK, generated by speakeasy cli .speakeasy/workflow.yaml # Speakeasy workflow file that dictates mapping of sources (eg: openapi docs) and targets (eg: language sdks) to generate serviceA/ - gen.yaml # Generation config for the ServiceA SDK serviceB/ - gen.yaml # Generation config for the ServiceB SDK ``` This structure can be expanded to accommodate any number of SDKs. ## Creating Your SDK Monorepo You have two options for creating your SDK monorepo with Speakeasy and GitHub: starting from a template or building from scratch. For the purpose of this guide, we will use an example with two APIs, one for `lending` and one for `accounting`. ### Option 1: Use a Template Start by cloning the [`template-sdk-monorepo` repository](https://github.com/speakeasy-sdks/template-sdk-monorepo?tab=readme-ov-file) using the "Use template" button on GitHub, and name it as you see fit. ### Option 2: Build from Scratch 1. Create a new repository on GitHub and clone it down locally with `git clone `. 2. Mimic the directory structure shown above. This can be achieved easily by following the interactive CLI commands below a. Install Speakeasy CLI b. Use quickstart to boostrap the repository. This will create the necessary directories and files for you to start generating SDKs. ```bash speakeasy quickstart ``` When prompted make sure to choose a sub directory rather than the root of your directory for generating the first target. We will add more targets in the following steps. c. Configure your sources ```bash speakeasy configure sources ``` For each source reference a local or remote OpenAPI document. Optionally add in an overlay if needed. Each source you configure here will be used to point towards a generation target in the following step. ![Screenshot of configuring sources for monorepo.](/assets/guides/monorepo-step-2c.png) d. Configure your targets For each target referenced a local or remote OpenAPI document. Optionally add in an overlay if needed. For ever target make sure to choose a a language, a source and a subdirectory to generate the target in. In the provided example `accounting` and `lending` are two sub directories in which we'll generate two different targets for two different sources. ```bash speakeasy configure targets ``` ![Screenshot of configuring targets for monorepo.](/assets/guides/monorepo-step-2d.png) #### Final Speakeasy Workflow The final speakeasy workflow file will look like the following ```yaml workflowVersion: 1.0.0 speakeasyVersion: latest sources: accounting: inputs: - location: /path/to/accounting/openapi.yaml lending: inputs: - location: /path/to/lending/openapi.yaml targets: accounting-ts: target: typescript source: accounting output: ./accounting lending-ts: target: typescript source: lending output: ./lending ``` #### Setup Github Release and Publishing Workflows Finally if you want your SDKs to be regenerated and published in their Github repos setup the workflow files needed for remote generation and publishing. The CLI can help you set up these files with: ```bash speakesy configure publishing ``` Follow the prompts to setup your secrets in Github Action Secrets. Push up the repo to your remote location and watch everything spin! ### Configure Generation Edit the `gen.yaml` file in the root of each SDK's subfolder. This file dictates the generator settings, including package name, class names, parameter inlining, and other preferences. For complete documentation on all the available publishing configurations, see [here](/docs/speakeasy-reference/generation/gen-yaml). ## Generate SDKs ### Locally Simply run `speakeasy run` and select the SDKs you wish to regenerate. Use `speakaesy run --force` if there are no OpenAPI document changes. ![Screenshot of running all targets for monorepo.](/assets/guides/monorepo-step-run.png) ### Github To manually trigger SDK generation: 1. Navigate to the Actions tab in your GitHub repository. 2. Select the generation workflow for the SDK you wish to generate. 3. Click "Run workflow" to start the generation process. Check mark the "Force" input option if there are no OpenAPI document changes. ## Verify your SDK After generation, review the SDK to ensure it aligns with your requirements. # Generate SDK in a Subdirectory Source: https://speakeasy.com/guides/sdks/generate-in-a-subdirectory import { Screenshot } from "@/mdx/components"; import subdirectoryImage1 from '../assets/subdirectory/1.png' import subdirectoryImage2 from '../assets/subdirectory/2.png' import subdirectoryImage3 from '../assets/subdirectory/3.png' import subdirectoryImage4 from '../assets/subdirectory/4.png' import subdirectoryImage5 from '../assets/subdirectory/5.png' Similar to setting up a monorepo, you can also configure Speakeasy to generate your SDK into a subdirectory. This setup maybe useful in maintaining seperation between handwritten and generated code or for consuming the SDK in a monolithic codebase. ## Step 1: In the root of your existing directory run `speakeasy quickstart`: ## Step 2: Select your generation language. ## Step 3: Name your package. ## Step 4: If you're setting your SDK up in a folder with existing subfolders, you will be prompted to select an output directory. ## Step 5: Complete generating your SDK. Going forward to generate your SDK navigate to your SDK folder and run `speakeasy run`. # Creating Internal and External SDKs Source: https://speakeasy.com/guides/sdks/overlays/internal-external-versions To create two different versions of an SDK, one for internal use and one for external use, use JSONPath expressions in [OpenAPI Overlays](/openapi/overlays) (a standard extension for modifying existing OpenAPI documents without changing the original). This approach dynamically targets and hides internal operations and parameters from the public SDK. The [`workflow.yaml` file](/docs/workflow-file-reference) can be configured to include Overlays as part of the source definition. ### Using the `x-internal` Extension First, add an `x-internal: true` extension to all the operations, parameters, and other elements in the OpenAPI spec that should only be available in the internal SDK. ```yaml filename="api.yaml" info: title: Sample API version: 1.0.0 description: A sample API with internal paths and parameters. paths: /public-resource: get: summary: Retrieve public data responses: '200': description: Public data response content: application/json: schema: type: object properties: id: type: string name: type: string /internal-resource: x-internal: true # This path is internal and should not be exposed externally get: summary: Retrieve internal data responses: '200': description: Internal data response content: application/json: schema: type: object properties: id: type: string internalInfo: type: string description: Internal information x-internal: true # This field is internal description: "This endpoint is restricted for internal staff management and not visible in public SDKs." ``` ### Using JSONPath Expressions in an Overlay Next, use a JSONPath expression to remove all the internal paths and parameters from the SDK. This removal occurs specifically when generating the internal SDK. ```yaml filename="internal-overlay.yaml" info: title: Sample API Overlay version: 1.0.0 actions: - target: $.paths.*.*[?(@["x-internal"] == true)] remove: true - target: $.paths.*.*[?(@.properties[?(@["x-internal"] == true)])] remove: true ``` Define a workflow that generates both the internal and external SDKs. ```yaml filename="workflow.yaml" workflowVersion: 1.0.0 speakeasyVersion: latest sources: external-api: inputs: - location: ./api.yaml overlays: - location: ./external-overlay.yaml internal-api: inputs: - location: ./api.yaml overlays: - location: ./internal-overlay.yaml targets: internal-sdk: target: python source: internal-api external-sdk: target: python source: external-api ``` # What are JSONPath Expressions? Source: https://speakeasy.com/guides/sdks/overlays/json-path-expressions JSONPath expressions provide a powerful tool for querying and manipulating JSON data in your OpenAPI specifications. By using JSONPath, you can selectively target specific nodes in your API spec and apply modifications without altering the base structure ## Example JSONPath Expressions * [All Post or Get Operations](#all-post-or-get-operations) * [Operations with a Request Body](#operations-with-a-request-body) * [Operations with a Specific Visibility Notation](#operations-with-a-specific-visibility-notation) * [Inject Annotations as Siblings](#inject-annotations-as-siblings) ### All Post or Get Operations This expression selects all paths that contain either POST or GET operations. It's useful when you want to apply changes or add annotations specifically to these HTTP methods. **JSONPath Target** ``` $.paths.*[?("post","get")] ``` **Overlay Action** ```yaml actions: - target: $.paths.*[?("post","get")] update: x-apiture-logging: true description: "Logging enabled for all POST and GET operations." ``` ### Operations with a Request Body This targets all operations that include a request body. It's ideal for adding descriptions, examples, or additional schema validations to request bodies. **JSONPath Target** ``` $.paths.*[?(@.requestBody)] ``` **Overlay Action** ```yaml actions: - target: $.paths.*[?(@.requestBody)] update: x-custom-validation: "custom-validation-schema" description: "Custom validations applied to request bodies." ``` ### Operations with a Specific Visibility Notation This expression is used to find all operations marked with 'admin' visibility. It can be used to apply additional security measures or modify documentation for admin-only endpoints. **JSONPath Target** ``` $.paths.*[?(@["x-visibility"] == "admin")] ``` **Overlay Action** ```yaml actions: - target: $.paths.*[?(@["x-visibility"] == "admin")] update: x-custom-security: "enhanced" description: "Enhanced security for admin endpoints." ``` ### Inject Annotations as Siblings This expression targets all operations within an OpenAPI specification that have an `operationId` property. **JSONPath Target** ``` $.paths.*.*[?(@.operationId)] ``` **Overlay Action** ```yaml actions: - target: $.paths.*.*[?(@.operationId)] update: x-detailed-logging: "enabled" log_level: "verbose" description: "Verbose logging enabled for this operation to track detailed performance metrics." ``` # Example Overlays Source: https://speakeasy.com/guides/sdks/overlays/overlays [Overlays](/docs/prep-openapi/overlays/create-overlays) act as a layer on top of your existing OpenAPI specification, allowing you to apply modifications and extensions seamlessly. They are perfect for adding new features, customizing responses, and adapting specifications to specific needs without disrupting the underlying structure. To demonstrate the power of overlays, we'll use a sample OpenAPI Specification for "The Speakeasy Bar." This initial spec sets the stage for our overlay examples: ```yaml filename="openapi.yaml" openapi: 3.1.0 info: title: The Speakeasy Bar version: 1.0.0 summary: A bar that serves drinks. servers: - url: https://speakeasy.bar description: The production server. security: - apiKey: [] tags: - name: drinks description: The drinks endpoints. - name: orders description: The orders endpoints. paths: /dinner/: post: ... get: ... /drinks/: post: ... ``` Let's explore how overlays can enhance and adapt this specification to do the following: * [Adding Speakeasy Extensions](#adding-speakeasy-extensions) * [Adding SDK Specific Documentation](#adding-sdk-specific-documentation) * [Modifying AutoGenerated Schemas](#modifying-autogenerated-schemas) * [Adding Examples to API Documentation](#adding-examples-to-api-documentation) * [Hiding Internal APIs from a Public SDK](#hiding-internal-apis-from-a-public-sdk) * [Removing specific PUT operation](#removing-specific-put-operation) * [Standardize Configurations](#standardize-configurations) ### Adding Speakeasy Extensions **Objective:** Integrate [Terraform](/docs/terraform/customize/entity-mapping) functionality into the API specification for order management. **Overlay Action**: ```yaml filename="overlay.yaml" overlay: 1.0.0 info: title: Add Terraform Functionality to Order Schema version: 1.1.0 actions: - target: "$.components.schemas.Order" update: - x-speakeasy-entity: Order description: "Enables Terraform provider support for the Order schema." ``` ### Adding SDK Specific Documentation **Objective:** Provide tailored instructions for Java and JavaScript SDKs for the `/orders` endpoint. **Overlay Action**: ```yaml filename="overlay.yaml" overlay: 1.0.0 info: title: Distinguish Order Endpoint Docs for Java and JavaScript SDKs version: 1.1.1 actions: - target: "$.paths['/orders'].post.description" update: - value: "For Java SDK: use `OrderService.placeOrder()`. For JavaScript SDK: use `orderService.placeOrder()`." ``` ### Modifying Autogenerated Schemas **Objective:** Enhance the precision of the Drink schema, making it more descriptive and informative for API consumers. ```yaml filename="overlay.yaml" overlay: 1.0.0 info: title: Refine Drink Schema for Better Clarity version: 1.1.2 actions: - target: "$.components.schemas.Drink" update: - properties: type: type: string description: "Type of drink, e.g., 'cocktail', 'beer'." alcoholContent: type: number description: "Percentage of alcohol by volume." ``` ### Adding Examples to API Documentation **Objective:** Illustrate the drink ordering process with a practical example for user clarity. ```yaml filename="overlay.yaml" overlay: 1.0.0 info: title: Add Drink Order Example for User Clarity version: 1.1.3 actions: - target: "$.paths['/drinks/order'].post" update: - examples: standardOrder: summary: "Standard order example" value: drink: "Old Fashioned" quantity: 1 ``` ### Hiding Internal APIs from a Public SDK **Objective:** Restrict the visibility of internal staff management endpoints. ```yaml filename="overlay.yaml" overlay: 1.0.0 info: title: Secure Internal Staff Management Endpoint version: 1.1.4 actions: - target: "$.paths['/internal/staff']" update: - x-internal: true description: "This endpoint is restricted for internal staff management and not visible in public SDKs." ``` ### Removing Specific Put Operation **Objective:** Exclude PUT operations without the `x-speakeasy-entity-operation.` **Overlay Action**: ```yaml filename="overlay.yaml" overlay: 1.0.0 info: title: Remove Non-Essential PUT Operations version: 1.1.0 actions: - target: $.paths.*.put[?(! @.x-speakeasy-entity-operation)] remove: true ``` ### Standardize Configurations **Objective:** Remove the server and security configurations from each operation within the paths. **Overlay Action**: ```yaml filename="overlay.yaml" overlay: 1.0.0 info: title: Standardize Server and Security Configurations version: 1.1.0 actions: - target: $.paths.*.*.servers remove: true - target: $.paths.*.*.security remove: true ``` # override-compile Source: https://speakeasy.com/guides/sdks/override-compile ## Overriding Compile Commands for SDK Generation ### 1. Remove `package.json` from `.genignore` The `.genignore` file is used to signal which files are manually managed rather than handled by the SDK generator. It functions similarly to `.gitignore` but for SDK generation purposes. Update your `.genignore` file to remove `package.json`. It no longer needs to be ignored as the generation process will manage it automatically. ### 2. Create a Compile Script Create a file named `openapi/scripts/compile.sh` and add the following script: ```bash #!/usr/bin/env bash set -e npm install npm run build ``` Ensure the script is executable by running the following command: ```bash chmod +x openapi/scripts/compile.sh ``` ### 3. Update `gen.yaml` Modify your `.speakeasy/gen.yaml` file to include the `compileCommand` under the TypeScript section. Add the following configuration: ```yaml typescript: compileCommand: - bash - -c - ./openapi/scripts/compile.sh ``` ### 4. Verify the Configuration Run the following command to test that the setup is working as expected: ```bash speakeasy run --force ``` # Switching default package manager to `pnpm` Source: https://speakeasy.com/guides/sdks/pnpm-default import { Callout } from "@/mdx/components"; ## Prerequisite - A GitHub repository with the Speakeasy [SDK Generation GitHub Action](https://github.com/speakeasy-api/sdk-generation-action) integrated and enabled. ## Adding `pnpm` Support 1. Open the GitHub Actions workflow file (e.g., `.github/workflows/sdk-generation.yaml`). 2. Modify the `generate` job to include the `pnpm_version` input. This ensures pnpm is installed during the action. ### Example Workflow File ```yaml name: Generate permissions: checks: write contents: write pull-requests: write statuses: write "on": workflow_dispatch: inputs: force: description: Force generation of SDKs type: boolean default: false set_version: description: Optionally set a specific SDK version type: string jobs: generate: uses: speakeasy-api/sdk-generation-action/.github/workflows/workflow-executor.yaml@v15 with: force: ${{ github.event.inputs.force }} mode: pr set_version: ${{ github.event.inputs.set_version }} speakeasy_version: latest working_directory: packages/sdk pnpm_version: "9.19.4" # Specify the required pnpm version secrets: github_access_token: ${{ secrets.GITHUB_TOKEN }} npm_token: ${{ secrets.NPM_TOKEN_ELEVATED }} speakeasy_api_key: ${{ secrets.SPEAKEASY_API_KEY }} ``` ## (Optional) Verifying `pnpm` Installation Ensure pnpm is used in the workflow by adding a step to verify its presence: ```yaml steps: - name: Verify pnpm installation run: pnpm --version ``` This outputs the installed `pnpm` version for confirmation during workflow execution. ## Additional Notes - Use the same `pnpm_version` as used in local development for consistency. - Ensure any `package.json` files are compatible with pnpm. Run `pnpm install` locally to verify. ## Using PNPM in Monorepos When working with monorepos, pnpm offers several advantages including strict module resolution and efficient workspace management. To configure Speakeasy to use pnpm in your monorepo: ### Local Development Configuration Add the `compileCommand` configuration to your `gen.yaml` file: ```yaml typescript: compileCommand: - pnpm - install ``` ### Workspace Configuration Ensure your `pnpm-workspace.yaml` includes the SDK directory: ```yaml packages: - "packages/*" - "sdk/*" # Include your SDK location ``` ### Benefits for Monorepos - **Strict module resolution**: Prevents dependency confusion between packages - **Efficient storage**: Shared dependencies across workspace packages - **Better workspace support**: Built-in monorepo tooling For comprehensive monorepo setup and troubleshooting tips, see our [TypeScript Monorepo Tips guide](/guides/sdks/typescript-monorepo-tips). # Tips for Integrating a TypeScript SDK into a Monorepo Source: https://speakeasy.com/guides/sdks/typescript-monorepo-tips import { Callout } from "@/mdx/components"; If you're looking to set up a monorepo from scratch, check out our [Create a monorepo](/guides/sdks/creating-a-monorepo) guide first. ## Dependency Confusion The most common issue we see is dependency confusion. This happens when developers have multiple versions of the same dependency across different packages in the monorepo. ### Why is this a problem? Let's say a developer has Zod installed in their monorepo's root, and their Speakeasy SDK also uses Zod. With Speakeasy's bundled Zod dependency, this is no longer an issue since the SDK uses its own version of Zod v3, preventing conflicts with user installations of Zod v4. Previously, developers might have run into a situation where code like this didn't work: ```typescript try { const result = await sdk.products.create({ name: "Cool Product", price: "not a number", // This should fail validation }); } catch (err) { if (err instanceof ZodError) { // This check could fail with peer dependencies! console.log("Validation error:", err.errors); } } ``` The `instanceof` check could fail because they were importing `ZodError` from a different instance of Zod than what the SDK was using. By bundling Zod, Speakeasy eliminates this confusion. ### How to fix it PNPM's strict module resolution is fantastic at preventing this issue: ```bash # In .npmrc shamefully-hoist=false strict-peer-dependencies=true ``` If you're using Yarn or npm, you'll need to be more explicit: ```json // With Yarn (in package.json) { "resolutions": { "zod": "^3.22.4", "@tanstack/react-query": "^4.29.5" } } // With npm (in package.json) { "overrides": { "zod": "^3.22.4", "@tanstack/react-query": "^4.29.5" } } ``` This forces all packages to use the same version of these dependencies, preventing the confusion. ## Module format mismatches - ESM vs CommonJS chaos The second most common issue is dealing with mixed module formats. Some packages use CommonJS, others use ESM, and a monorepo probably has a mix of both. ### Why this is a problem You might see errors like: ``` Error [ERR_REQUIRE_ESM]: require() of ES Module not supported ``` Or: ``` SyntaxError: Cannot use import statement outside a module ``` These happen when module systems don't align. It's especially common when there's a package using CommonJS trying to import an ESM module, or vice versa. ### How to fix it The simplest solution is to configure your Speakeasy SDK to use ESM format: ```yaml # In gen.yaml typescript: moduleFormat: esm ``` If you need to support both ESM and CommonJS packages importing your SDK, you can use the dual format (which is the Speakeasy default): ```yaml # In gen.yaml typescript: moduleFormat: dual ``` When using `moduleFormat: dual`, make sure the tsconfig.json is set up correctly: ```json // In tsconfig.json { "compilerOptions": { "moduleResolution": "node", "module": "esnext", "esModuleInterop": true } } ``` There's a small bundle size tradeoff with dual format, but it's usually worth it for the compatibility benefits. ## Package manager differences - npm vs the world The last common issue is package manager compatibility. Speakeasy uses npm when building SDKs, but a monorepo might use pnpm, Yarn, or even Bun. ### Why this is a problem Different package managers handle dependencies differently. This can lead to subtle issues where the SDK works fine when generated, but breaks when integrated into the monorepo. For example, one might see errors about missing dependencies that they know are installed, or weird resolution issues that only happen in the monorepo. ### How to fix it Configure Speakeasy to use your preferred package manager when building the SDK. For pnpm (recommended for monorepos), customize the compile command in your `gen.yaml`: ```yaml # For pnpm typescript: compileCommand: - pnpm - install ``` This tells Speakeasy to use pnpm instead of npm when building the SDK, which is especially important in monorepos where pnpm's strict module resolution helps prevent dependency confusion issues. For more detailed information about configuring pnpm as your default package manager, see our [Using PNPM guide](/guides/sdks/pnpm-default). ## Putting it all together - a real-world example This is an example of how a developer may set up a monorepo with a Speakeasy SDK: ``` my-monorepo/ ├── package.json ├── pnpm-workspace.yaml ├── tsconfig.json ├── packages/ │ ├── api-sdk/ # Speakeasy-generated SDK │ │ ├── package.json │ │ ├── tsconfig.json │ │ └── gen.yaml │ ├── frontend/ # Frontend application using the SDK │ │ ├── package.json │ │ └── tsconfig.json │ └── backend/ # Backend service using the SDK │ ├── package.json │ └── tsconfig.json ``` pnpm-workspace.yaml: ```yaml packages: - "packages/*" ``` frontend/package.json: ```json { "dependencies": { "api-sdk": "workspace:*" } } ``` gen.yaml: ```yaml typescript: moduleFormat: esm compileCommand: - pnpm - install - and - pnpm - build ``` This setup gives: 1. A consistent dependency tree with pnpm's strict module resolution 2. ESM modules for maximum compatibility 3. pnpm for package management ## Best practices for TypeScript SDKs in monorepos Beyond the specific issues above, here are some general best practices: - **Use workspace references**: Always reference your SDK using workspace syntax (`workspace:*`) rather than local file paths - **Consistent TypeScript versions**: Use the same TypeScript version across all packages - **Shared tsconfig**: Create a base tsconfig.json that all packages extend - **Centralized types**: Consider creating a shared types package for common interfaces - **Integration tests**: Write tests that verify the SDK works correctly with other packages ## Related documentation For more information on configuring Speakeasy TypeScript SDKs, check out: - [TypeScript SDK Design](https://www.speakeasy.com/docs/languages/typescript/methodology-ts) - [Configuring Module Format](https://www.speakeasy.com/docs/customize/typescript/configuring-module-format) - [TypeScript SDK Reference](https://www.speakeasy.com/docs/languages/typescript/feature-support) - [Model Validation and Serialization](https://www.speakeasy.com/docs/customize/typescript/model-validation-and-serialization) - [TypeScript Configuration](https://www.speakeasy.com/docs/speakeasy-reference/generation/ts-config) # Utilizing User Agent Strings for Telemetry Source: https://speakeasy.com/guides/sdks/utilizing-user-agent-strings ## Overview Each Speakeasy SDK incorporates a unique user agent string in all HTTP requests made to the API. This string helps identify the source SDK and provides details like the SDK version, the generator version, the document version, and the package name. ## Format The format of the user agent string across all Speakeasy SDKs is as follows: ``` speakeasy-sdk/<> {{SDKVersion}} {{GenVersion}} {{DocVersion}} {{PackageName}} ``` Components: * `Language`: The language of the SDK (e.g., C#, Java, Python). * `SDKVersion`: The version of the SDK, specified in `gen.yaml`. * `GenVersion`: The version of the Speakeasy generator used. * `DocVersion`: The version of the OpenAPI document that generated the SDK. * `PackageName`: The name of the package as defined in `gen.yaml`. For a Java SDK, the user agent string might look like this: ```speakeasy-sdk/java 2.3.1 1.5.0 2022.01 1.0.0 speakeasyJavaClient``` ## Parsing User Agent Strings To accurately parse user agent strings, you can use regular expressions, string manipulation techniques, or dedicated parsing libraries. Below is a Python example using regex and Flask: ```python from flask import request import re @app.route('/some-path') def some_function(): user_agent = request.headers.get('User-Agent') # Regex pattern to match the user agent string pattern = r"^speakeasy-sdk/(?P\w+)\s+(?P[\d\.]+)\s+(?P[\d\.]+)\s+(?P\S+)\s+(?P[\w\.\-]+)$" # Match the user agent string against the regex pattern match = re.match(pattern, user_agent) if match: details = match.groupdict() print(details) ``` ## Utilizing Parsed Data for Telemetry Parsed user agent strings can enhance telemetry in several ways: **Data Enrichment**: With parsed user agent strings, you can enhance telemetry records by appending key metadata such as the SDK version, programming language, and package details. This enriched data supports more detailed segmentation and sophisticated analysis, improving the granularity and usability of your telemetry insights. This approach allows you to track feature adoption across different segments and tailor your development focus accordingly. **Monitoring SDK Usage**: Utilize the detailed data from user agent strings to monitor the adoption rates of different SDK versions and the prevalence of programming languages among your users. This intelligence is crucial for informed decision-making regarding SDK updates and deprecation schedules. By understanding which versions are most popular and how quickly users adopt new releases, you can better manage support resources and communication strategies. **Performance Analysis**: Correlate specific SDK versions or configurations with performance metrics like response times and error rates. This analysis helps pinpoint whether recent updates have improved performance or introduced new issues, allowing your development team to target fixes and optimizations more effectively. Regularly reviewing these correlations helps maintain high performance standards and ensures a positive user experience. **Anomaly Detection**: Implement advanced anomaly detection techniques in your telemetry to automatically flag unusual activity, such as a sudden decrease in the usage of a normally popular SDK version or an unexpected increase in error rates following a new release. Early detection of these anomalies can prompt swift action, potentially averting more significant issues and enhancing customer satisfaction. This proactive monitoring is essential in maintaining the reliability and credibility of your SDKs. # What is a Terraform Provider Source: https://speakeasy.com/guides/terraform/crud A Terraform provider is a plugin that extends Terraform, allowing it to manage external resources such as cloud services. It serves as a mediator between Terraform and external APIs, using the [Terraform Plugin Protocol](https://developer.hashicorp.com/terraform/plugin/terraform-plugin-protocol) for communication. Terraform providers, built with the [terraform-plugin-framework](https://github.com/hashicorp/terraform-plugin-framework), include: 1. **Resources** and **Data Sources**: described using a Terraform Type Schema, which is a `map[string]Attribute`, where an Attribute could be _Primitive_, _Composite_, or _List_. 2. **Create**, **Read**, **Update** and **Delete** methods: the interface through which the provider interacts with an external resource (usually an API) to reconcile a desired terraform specification with the actual state of the resource. 3. **Plan Validators**: defined to enable the validation of a desired specification at Plan-time, without making API calls to the external resource. 4. **Plan Modifiers**: defined to enable custom semantics around the Terraform Type Schema, and how it is reconciled with the external state to make a Plan. 5. **Resource imports**: defined to enable Terraform Specifications to be generated from existing resources. ## A simple CRUD example Let's explore how to define a resource and map API operations to Terraform methods using annotations for CRUD actions. ### Defining a Resource Use `x-speakeasy-entity` to define a resource that you want to use terraform to manage. ```yaml filename="openapi.yaml" /drinks: post: x-speakeasy-entity-operation: Drink#create requestBody: required: true content: application/json: schema: $ref: "#/components/schemas/Drink" responses: "200": content: application/json: schema: $ref: "#/components/schemas/Drink" /drinks/{id}: parameters: - name: id in: path required: true schema: type: string get: x-speakeasy-entity-operation: Drink#read responses: "200": content: application/json: schema: $ref: "#/components/schemas/Drink" post: x-speakeasy-entity-operation: Drink#update requestBody: required: true content: application/json: schema: $ref: "#/components/schemas/Drink" responses: "200": content: application/json: schema: $ref: "#/components/schemas/Drink" delete: x-speakeasy-entity-operation: Drink#delete responses: "202": description: OK components: schemas: Drink: x-speakeasy-entity: Drink type: object properties: name: description: The name of the drink. type: string examples: - Old Fashioned - Manhattan - Negroni type: $ref: "#/components/schemas/DrinkType" price: description: The price of one unit of the drink in US cents. type: number examples: - 1000 # $10.00 - 1200 # $12.00 - 1500 # $15.00 required: - name - price ``` ### Mapping API Operations to Resources Methods An OpenAPI specification tracks a large list of [`Operation` objects](https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.1.0.md#operationObject). For a Terraform Provider generated by Speakeasy, the key element is the [the `x-speakeasy-entity-operation` annotation](/docs/terraform/customize/entity-mapping). This annotation clarifies the purpose of each operation in terms of how it affects the associated remote entity. ```yaml filename="openapi.yaml" /drinks: post: x-speakeasy-entity-operation: Drink#create requestBody: required: true content: application/json: schema: $ref: "#/components/schemas/Drink" responses: "200": content: application/json: schema: $ref: "#/components/schemas/Drink" /drinks/{id}: parameters: - name: id in: path required: true schema: type: string get: x-speakeasy-entity-operation: Drink#read responses: "200": content: application/json: schema: $ref: "#/components/schemas/Drink" post: x-speakeasy-entity-operation: Drink#update requestBody: required: true content: application/json: schema: $ref: "#/components/schemas/Drink" responses: "200": content: application/json: schema: $ref: "#/components/schemas/Drink" delete: x-speakeasy-entity-operation: Drink#delete responses: "202": description: OK components: schemas: Drink: x-speakeasy-entity: Drink type: object properties: name: description: The name of the drink. type: string examples: - Old Fashioned - Manhattan - Negroni id: description: The ID of the drink readOnly: true type: string type: $ref: "#/components/schemas/DrinkType" price: description: The price of one unit of the drink in US cents. type: number examples: - 1000 # $10.00 - 1200 # $12.00 - 1500 # $15.00 required: - name - price ``` ## Managing Complex API Semantics with Speakeasy APIs can have unique semantics not fully described by OpenAPI specs. To address this we have: 1. **Inference Rules**: Automatically derive most API semantics from the OpenAPI spec, with the exception of the `x-speakeasy-entity-operation` annotation. 2. **OpenAPI Extensions**: For more complex cases, use extensions to provide detailed configurations. These extensions are documented in the [Terraform Extensions](/docs/terraform/extensions/) section of this documentation. 3. **Support**: Our engineering team continually updates inference rules and extensions to accommodate new API patterns. Read more in our [documentation here](/docs/create-terraform), or [view more examples here](/guides). # What is hoisting? Source: https://speakeasy.com/guides/terraform/hoisting Hoisting is a technique used in API design to reorganize the structure of data in API requests and responses. Its main goal is to simplify the way data is presented by moving nested or deeply structured data to a higher level in the API's response or request body. This process makes the data easier to work with for developers by reducing complexity and aligning the structure more closely with how resources are conceptually understood. In essence, hoisting "flattens" data structures. For APIs, this means transforming responses or requests so that important information is more accessible and not buried within nested objects. This is particularly beneficial when dealing with APIs that serve complex data models, as it can make the data easier to parse and use without extensive traversal of nested objects. ## When should you use hoisting? Hoisting is usually applied in specific scenarios to improve the design and usability of APIs: - **Complex Nested Structures**: Employ hoisting when your API deals with complex, deeply nested data. It streamlines access to important information, reducing the need for deep navigation. - **Frequent Data Access**: Use hoisting for elements that are often accessed or critical to operations, making them more directly accessible. - **Data Model Alignment**: Apply hoisting to better align the API's data structure with the conceptual model of the resources. ## Initial Structure: Without Hoisting Initially, our data structure represents a hierarchical model with nested entities, depicted as a tree. `x-speakeasy-entity: 1` at the top, with entities 2 and 3 as direct descendants. Entity 2 further nests entities 4, which branches into 5 and 6. {/* prettier-ignore */} ```bash (1) / \ 2 3 \ 4 / \ 5 6 ``` ## Step 1: Selecting an entity for hoisting Entity (2) is marked with `x-speakeasy-entity` for hoisting. {/* prettier-ignore */} ```bash 1 / \ (2) 3 \ 4 / \ 5 6 ``` ## Step 2 After applying hoisting, the structure is reorganized to prioritize `x-speakeasy-entity: 2`, making its leaf nodes directly accessible and flattening the remaining structure. `x-speakeasy-entity: 2` {/* prettier-ignore */} ```bash x-speakeasy-entity: 2 (2) / \ 3 4 / \ 5 6 ``` ## Real-World Application: Flattening a "data" property The JSON Schemas for `Drink`, `Drink`, and `{ drinkType: $DrinkType }` will be each considered the root of a Terraform Type Schema, and will be merged together to form the final Terraform Type Schema using Attribute Inference. However, this is not always desired. Consider this alternative response: ## Original In the original API design, the request and response body are structured equivalently, without nested elements. This approach is straightforward but might not always suit complex data relationships or requirements. ```yaml filename="openapi.yaml" /drinks: post: x-speakeasy-entity-operation: Drink#create parameters: - name: type in: query description: The type of drink required: false deprecated: true schema: $ref: "#/components/schemas/DrinkType" requestBody: required: true content: application/json: schema: $ref: "#/components/schemas/Drink" responses: "200": content: application/json: schema: $ref: "#/components/schemas/Drink" "5XX": $ref: "#/components/responses/APIError" default: $ref: "#/components/responses/UnknownError" ``` ## Alternate The alternate approach introduces a nested `data` property in the API response, which can encapsulate the drink information more distinctly, albeit adding a layer of complexity in data access. ```yaml filename="openapi.yaml" /drinks: post: x-speakeasy-entity-operation: Drink#create parameters: - name: type in: query description: The type of drink required: false deprecated: true schema: $ref: "#/components/schemas/DrinkType" requestBody: required: true content: application/json: schema: $ref: "#/components/schemas/Drink" responses: "200": content: application/json: schema: type: object properties: data: $ref: "#/components/schemas/Drink" "5XX": $ref: "#/components/responses/APIError" default: $ref: "#/components/responses/UnknownError" ``` ## Original Code Using the same request/response bodies, speakeasy would generate the following type schema ```go filename="drink_resource.go" func (r *DrinkResource) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) { resp.Schema = schema.Schema{ MarkdownDescription: "Drink Resource", Attributes: map[string]schema.Attribute{ "drink_type": schema.StringAttribute{ Optional: true, Computed: true Description: `The type of drink.`, stringvalidator.OneOf( "cocktail", "non-alcoholic", "beer", "wine", "spirit", "other", ), }, "name": schema.Int64Attribute{ Required: true, Description: `The name of the drink.`, }, "price": schema.StringAttribute{ Required: true, Description: `The price of one unit of the drink in US cents.`, }, }, } } ``` ## Alternative Code When generated, the provider schema would look like this: not understanding that `data` was a nested object, and instead treating it as a root of the schema. ```go filename="drink_resource.go" func (r *DrinkResource) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) { resp.Schema = schema.Schema{ MarkdownDescription: "Drink Resource", Attributes: map[string]schema.Attribute{ "drink_type": schema.StringAttribute{ Optional: true, Description: `The type of drink.`, stringvalidator.OneOf( "cocktail", "non-alcoholic", "beer", "wine", "spirit", "other", ), }, "name": schema.Int64Attribute{ Required: true, Description: `The name of the drink.`, }, "price": schema.StringAttribute{ Required: true, Description: `The price of one unit of the drink in US cents.`, }, "data": schema.SingleNestedAttribute{ Computed: true, Attributes: map[string]schema.Attribute{ "drink_type": schema.StringAttribute{ Computed: true Description: `The type of drink.`, }, "name": schema.Int64Attribute{ Computed: true Description: `The name of the drink.`, }, "price": schema.StringAttribute{ Computed: true, Description: `The price of one unit of the drink in US cents.`, }, }, }, }, } } ``` ## The Fix: Implementing Hoisting By applying the [`x-speakeasy-entity` annotation](/docs/terraform/customize/entity-mapping), we direct the schema generation process to consider the `Drink` schema as the root of the type schema, effectively flattening the response structure for easier access. ```yaml filename="openapi.yaml" /drinks: post: x-speakeasy-entity-operation: Drink#create parameters: - name: type in: query description: The type of drink required: false deprecated: true schema: $ref: "#/components/schemas/DrinkType" requestBody: required: true content: application/json: schema: $ref: "#/components/schemas/Drink" responses: "200": content: application/json: schema: type: object properties: data: $ref: "#/components/schemas/Drink" "5XX": $ref: "#/components/responses/APIError" default: $ref: "#/components/responses/UnknownError" components: schemas: Drink: x-speakeasy-entity: Drink type: object properties: name: description: The name of the drink. type: string examples: - Old Fashioned - Manhattan - Negroni type: $ref: "#/components/schemas/DrinkType" price: description: The price of one unit of the drink in US cents. type: number examples: - 1000 # $10.00 - 1200 # $12.00 - 1500 # $15.00 required: - name - price ``` ## Finalized Schema: Simplified Access The final provider schema reflects a flattened structure, similar to the original API design but with the flexibility to include nested data when necessary. ```go filename="drink_resource.go" func (r *DrinkResource) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) { resp.Schema = schema.Schema{ MarkdownDescription: "Drink Resource", Attributes: map[string]schema.Attribute{ "drink_type": schema.StringAttribute{ Optional: true, Computed: true Description: `The type of drink.`, stringvalidator.OneOf( "cocktail", "non-alcoholic", "beer", "wine", "spirit", "other", ), }, "name": schema.Int64Attribute{ Required: true, Description: `The name of the drink.`, }, "price": schema.StringAttribute{ Required: true, Description: `The price of one unit of the drink in US cents.`, }, }, } } ``` # Creating a Merged Terraform Entity Source: https://speakeasy.com/guides/terraform/merged-entity Creating a merged Terraform entity involves combining data from separate API endpoints into a single Terraform resource. This process allows Terraform to manage complex entities that span multiple API calls for their lifecycle operations—create, read, update, and delete. ## Example Scenario: Merging Resource Entities Consider a scenario where managing a `drink` resource requires setting a `visibility` attribute post-creation using separate API endpoints: 1. **Create the drink**: Invoke `POST /drink` to create the drink entity. 2. **Set visibility**: Follow with `POST /drink/{id}/visibility` to configure visibility. ## Step 1: Define API Endpoints Identify the API endpoints involved in the operation. For instance, creating a `drink` and setting its visibility involves two distinct endpoints: ```yaml filename="openapi.yaml" /drinks: post: requestBody: required: true content: application/json: schema: type: object properties: name: type: string required: [name] responses: "200": content: application/json: schema: type: object properties: data: type: object properties: id: type: string required: [id] required: [data] /drink/{id}/visibility: post: requestBody: required: true content: application/json: schema: type: object properties: visibility: type: string enum: - public - private responses: "202": description: OK ``` ## Step 2: Annotate for Execution Order Mark both operations with annotations. For the operation requiring the `id` parameter, assign an `order` property value greater than the first operation to reflect its dependency on the `id` attribute. ```yaml filename="openapi.yaml" /drinks: post: x-speakeasy-entity-operation: Drink#create requestBody: required: true content: application/json: schema: type: object properties: name: type: string required: [name] responses: "200": content: application/json: schema: type: object properties: data: type: object properties: id: type: string required: [id] required: [data] /drink/{id}/visibility: post: x-speakeasy-entity-operation: Drink#create#2 requestBody: required: true content: application/json: schema: type: object properties: visibility: type: string enum: - public - private responses: "202": description: OK ``` ## Step 3: Configure Hoisting for Response Unwrapping Use `x-speakeasy-entity` annotations to simplify response handling by hoisting, avoiding nested data wrapping. ```yaml filename="openapi.yaml" /drinks: post: x-speakeasy-entity-operation: Drink#create requestBody: required: true content: application/json: schema: x-speakeasy-entity: Drink type: object properties: name: type: string required: [name] responses: "200": content: application/json: schema: type: object properties: data: type: object x-speakeasy-entity: Drink properties: id: type: string required: [id] required: [data] /drink/{id}/visibility: post: x-speakeasy-entity-operation: Drink#create#2 requestBody: required: true content: application/json: schema: type: object x-speakeasy-entity: Drink properties: visibility: type: string enum: - public - private responses: "202": description: OK ``` ## Advanced Example Step-by-Step When an [x-speakeasy-entity-operation](/docs/terraform/customize/entity-mapping) is defined, the request body, parameters, and response bodies of `CREATE` and `READ` operations are considered the root of the Terraform Type Schema. ## Step 1: Adding a `x-speakeasy-entity-operation: Drink#create` annotation marks the `POST /drinks` operation as CREATING a `drink` resource. ```yaml filename="openapi.yaml" /drinks: post: x-speakeasy-entity-operation: Drink#create parameters: - name: type in: query description: The type of drink required: false deprecated: true schema: $ref: "#/components/schemas/DrinkType" requestBody: required: true content: application/json: schema: $ref: "#/components/schemas/Drink" responses: "200": content: application/json: schema: $ref: "#/components/schemas/Drink" "5XX": $ref: "#/components/responses/APIError" default: $ref: "#/components/responses/UnknownError" components: schemas: Drink: type: object properties: name: description: The name of the drink. type: string examples: - Old Fashioned - Manhattan - Negroni type: $ref: "#/components/schemas/DrinkType" price: description: The price of one unit of the drink in US cents. type: number examples: - 1000 # $10.00 - 1200 # $12.00 - 1500 # $15.00 required: - name - price DrinkType: description: The type of drink. type: string enum: - cocktail - non-alcoholic - beer - wine - spirit - other ``` ## Step 2: Parameters, Request Bodies, and Response Bodies (associated with a 2XX status code) are each considered roots of the Terraform Type Schema ```yaml filename="openapi.yaml" /drinks: post: x-speakeasy-entity-operation: Drink#create parameters: - name: type in: query description: The type of drink required: false deprecated: true schema: $ref: "#/components/schemas/DrinkType" requestBody: required: true content: application/json: schema: $ref: "#/components/schemas/Drink" responses: "200": content: application/json: schema: $ref: "#/components/schemas/Drink" "5XX": $ref: "#/components/responses/APIError" default: $ref: "#/components/responses/UnknownError" components: schemas: Drink: type: object properties: name: description: The name of the drink. type: string examples: - Old Fashioned - Manhattan - Negroni type: $ref: "#/components/schemas/DrinkType" price: description: The price of one unit of the drink in US cents. type: number examples: - 1000 # $10.00 - 1200 # $12.00 - 1500 # $15.00 required: - name - price DrinkType: description: The type of drink. type: string enum: - cocktail - non-alcoholic - beer - wine - spirit - other ``` ## Step 3 The Terraform Type Schema merges all 3 of these together, and inferring links between the Operation and the Attributes. Note that similarly named attributes are merged together, and that the `DrinkType` attribute is inferred to be a `DrinkType` enum, rather than a `string`. ```yaml filename="derived.yaml" - create.parameters: type: from: "paths["/drinks"].post.parameters[0]" type: enum enumValues: type: string values: - cocktail - non-alcoholic - beer - wine - spirit - other - create.requestBody: name: from: "components.schemas.Drink.properties.name" description: The name of the drink. type: string examples: - Old Fashioned - Manhattan - Negroni drinkType: from: "components.schemas.Drink.properties.type" type: string enum: - cocktail - non-alcoholic - beer - wine - spirit - other price: from: "components.schemas.Drink.properties.price]" type: number - create.successResponseBody: name: from: "components.schemas.Drink.properties.name" description: The name of the drink. type: string examples: - Old Fashioned - Manhattan - Negroni drinkType: from: "components.schemas.Drink.properties.type" type: string enum: - cocktail - non-alcoholic - beer - wine - spirit - other price: from: "components.schemas.Drink.properties.price]" type: number ``` ## Step 4 These attributes are then merged together. If any properties conflict in type, an error is raised. ```yaml filename="derived.yaml" - create.requestShard: type: from: "paths["/drinks"].post.parameters[0]" type: enum enumValues: type: string values: - cocktail - non-alcoholic - beer - wine - spirit - other name: from: "components.schemas.Drink.properties.name" description: The name of the drink. type: string examples: - Old Fashioned - Manhattan - Negroni drinkType: from: "components.schemas.Drink.properties.type" type: string enum: - cocktail - non-alcoholic - beer - wine - spirit - other price: from: "components.schemas.Drink.properties.price]" type: number - create.responseShard: name: from: "components.schemas.Drink.properties.name" description: The name of the drink. type: string examples: - Old Fashioned - Manhattan - Negroni drinkType: from: "components.schemas.Drink.properties.type" type: string enum: - cocktail - non-alcoholic - beer - wine - spirit - other price: from: "components.schemas.Drink.properties.price]" type: number ``` ## Step 5 First, the `type` is taken from the OpenAPI `type`. It is `Optional: true` because it is not `required: true` or `nullable: true`. The description is taken from the OpenAPI `description`. The `examples` will be used to generate an example for the documentation ```go filename="drink_resource.go" func (r *DrinkResource) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) { resp.Schema = schema.Schema{ MarkdownDescription: "Drink Resource", Attributes: map[string]schema.Attribute{ "type": schema.StringAttribute{ Optional: true, Description: `The type of drink.`, }, }, } } ``` ## Step 6 Second, the `drinkType` is taken from the request and response bodies. It's converted to snake case as `drink_type` to follow terraform best-practices. It is `Optional: true` because it was not a member of the OpenAPI `requiredProperties`. It is `Computed: true` because even if not defined, the API is defined to (optionally) return a value for it. A plan validator is added with each of the enum values. This will be used to validate the plan at plan-time. ```go filename="drink_resource.go" func (r *DrinkResource) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) { resp.Schema = schema.Schema{ MarkdownDescription: "Drink Resource", Attributes: map[string]schema.Attribute{ "type": schema.StringAttribute{ Optional: true, Description: `The type of drink.`, }, "drink_type": schema.StringAttribute{ Optional: true, Computed: true Description: `The type of drink.`, stringvalidator.OneOf( "cocktail", "non-alcoholic", "beer", "wine", "spirit", "other", ), }, }, } } ``` ## Step 7 The other parameters are also pulled in from the request body. Both of them are required, with their type being derived from the equivalent Terraform primitive to their OpenAPI type. ```go filename="drink_resource.go" func (r *DrinkResource) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) { resp.Schema = schema.Schema{ MarkdownDescription: "Drink Resource", Attributes: map[string]schema.Attribute{ "type": schema.StringAttribute{ Optional: true, Description: `The type of drink.`, }, "drink_type": schema.StringAttribute{ Optional: true, Computed: true Description: `The type of drink.`, stringvalidator.OneOf( "cocktail", "non-alcoholic", "beer", "wine", "spirit", "other", ), }, "name": schema.Int64Attribute{ Required: true, Description: `The name of the drink.`, }, "price": schema.StringAttribute{ Required: true, Description: `The price of one unit of the drink in US cents.`, }, }, } } ``` ## Step 8: Cleanup In this API `drinkType` and `type` appear to refer to the same thing. `type` comes from a query parameter, whereas `drinkType` comes from the response body. This kind of pattern can be found in more legacy APIs, where parameters have been moved around and renamed, but older versions of those attributes are left around for backwards capability reasons. To clean up, we have many options we can apply to the API to describe what we want to happen: 1. `x-speakeasy-ignore: true` could be applied to the query parameter. After this point, it won't be configurable, and will never be sent. 2. `x-speakeasy-match: drinkType` could be applied to the query parameter. This will cause it to always be sent in the request, the same as the `drink_type` property. 3. `x-speakeasy-name-override: type` could be applied to the `drinkType` property. This will rename it as `type`, and ensure both `drinkType` request body key and `type` query parameter are both always sent. ```go filename="drink_resource.go" func (r *DrinkResource) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) { resp.Schema = schema.Schema{ MarkdownDescription: "Drink Resource", Attributes: map[string]schema.Attribute{ "drink_type": schema.StringAttribute{ Optional: true, Computed: true Description: `The type of drink.`, stringvalidator.OneOf( "cocktail", "non-alcoholic", "beer", "wine", "spirit", "other", ), }, "name": schema.Int64Attribute{ Required: true, Description: `The name of the drink.`, }, "price": schema.StringAttribute{ Required: true, Description: `The price of one unit of the drink in US cents.`, }, }, } } ``` # Overlay to remove non-Terraform endpoints Source: https://speakeasy.com/guides/terraform/remove-nontf-endpoints Often generating a Terraform provider from your OpenAPI spec means using a subset of endpoints to generate the provider. Endpoints or `paths` in your spec not used for terraform generation will still be used to generate a Go client inside your provider. This can lead to a bloated provider with unused endpoints. If you want to remove these consider using the following overlay. ```yaml overlay: 1.0.0 info: title: Overlay openapi.yaml version: 0.1.0 actions: - target: $.paths.*.*[?(!@.x-speakeasy-entity-operation && @.operationId)] # Find all paths that do not have x-speakeasy-entity-operation and operationId remove: true ``` When applied to an OpenAPI specification this overlay will remove any paths from the specification that are not tagged with `x-speakeasy-entity-operation`. The operation needed to map an OpenAPI `operationId` to a terraform resource. To apply this overlay run `speakeasy overlay apply -s {your-spec.yaml} -o {overlay.yaml}`. This will generate a new OpenAPI specification with the paths removed. Finally add this to your terraform generation workflow by using `speakeasy configure sources` to add an overlay to any source with an existing openapi specificaiton configured. # privacy-policy Source: https://speakeasy.com/legal/privacy-policy Privacy is important. This Privacy Policy explains practices regarding the collection, use and disclosure of information received through the Services. This Privacy Policy does not apply to any third-party websites, services or applications, even if accessible through the Services. In this Privacy Policy: - We'll refer to our website as the “Site”. - We'll refer to all the products and services we provide, individually and collectively, as the “Services”. - We'll refer to you, the person or entity accessing our Site or using our Services, as “you” or “your” or (if you are a purchaser of our Services), our “customer”. ## Definitions Data Controller: For general data protection regulation purposes, the “Data Controller” means the organization who decides the purposes for which, and the way in which, any Personal Information is processed. Our customers are the Data Controllers. Data Processor: A “Data Processor” is an organization which processes Personal Information for a Data Controller. We are the Data Processor for our customers. As a Data Processor, we are bound by the requirements of the General Data Protection Regulations (the “GDPR”). Data Processing: Data processing is any operation or set of operations (whether automated or not) performed upon Personal Information. Examples of data processing explicitly listed in the text of the GDPR are: collection, recording, organizing, structuring, storing, adapting, altering, retrieving, consulting, using, disclosing by transmission, disseminating or making available, aligning or combining, restricting, erasure or destruction. Personal Information: Personal information is any information which is about you, from which you can be identified. Personal Information includes information such as an individual's name, address, telephone number, or email address. Personal Information also includes information about an individual's activities, such as information about his or her activity on Site or our Services, and demographic information, such as date of birth, gender, geographic area, and preferences, when any of this information is linked to personal information that identifies that individual. Personal Information does not include "aggregate" or other non-personally identifiable information. Aggregate information is information that we collect about a group or category of products, services, or users that is not personally identifiable or from which individual identities are removed. ## How do we collect Personal Information? In our service as a Data Processor, we collect Personal Information from Data Controllers in several ways: - Information you provide to us directly. - Information we may receive from third parties. - We may receive information about you, including Personal information, from other third parties, and may combine this information with other personal information we maintain about you. If we do so, this Privacy Policy governs any combined information that we maintain in personally identifiable format. ## What information do we collect? We may collect the following types of personal information from you: - Your first and last name, username and email address. - Your company's name. - Your (and/or your company's) physical address. - Information you choose to provide us through our Services (including, for example, your birthdate and/or phone number). - We may collect information you post to, or collect from, users of the Services. We use this information to operate, maintain, and provide to you the features and functionality of the Services. - We may also collect and aggregate information about the use of our Site and our Services. That information includes browser and device data, such as IP address, device type, screen resolution, browser type, operating system name and version, language, as well as add-ons for your browser. The information may also include usage data, including the pages visited on and links clicked on our Site, the time spent on those pages, and the pages that led or referred you to our Site. - We may also permit third-party online advertising networks to collect information (through Cookies or similar tracking technology) about your and others' use of our Services and any of your mobile or web applications, in order to allow those third-party networks to display ads that may be relevant to your interests on our Services as well as on other websites or apps. ## What do we use your Personal Information for? We will use your Personal Information, in compliance with this Privacy Policy, to help us deliver the Services to you. Any of the information we collect from you may be used in the following ways: - To operate, maintain, and provide to you the features and functionality of the Services. - To compile statistics and analysis about use of our Site and our Services. - To personalize your experience. - To improve our Site and our Services — we continually strive to improve our site offerings based on the information and feedback we receive from you. - To improve customer service — your Personal Information helps us to more effectively respond to your customer service requests and support needs. - To send periodic emails — The email address you provide may be used to send you information, notifications that you request about changes to our Services, to alert you of updates, and to send periodic emails containing information relevant to your account. - If you purchase our Services, then to enable you to purchase, renew and appropriately use a commercial license to our Services. - We may also use Personal Information you provide to send you email marketing about Speakeasy products and services, invite you to participate in events or surveys, or otherwise communicate with you for marketing purposes. We allow you to opt-out from receiving marketing communications from us as described in the "Your Choices" section below. We may also use your Personal Information where necessary for us to comply with a legal obligation, including to share information with government and regulatory authorities when required by law or in response to legal process, obligation, or request. We cooperate with government and law enforcement officials or private parties to enforce and comply with the law. We may disclose your Personal Information to government or law enforcement officials or private parties as we believe necessary or appropriate: (i) to respond to claims, legal process (including subpoenas); (ii) to protect our property, rights and safety and the property, rights and safety of a third party or the public in general; and (iii) to stop any activity that we consider illegal, unethical or legally actionable activity. We will request your consent before we use or disclose your Personal Information for a materially different purpose than those set forth in this Policy. ## Your Choices About Your Personal Information We may use the information we collect or receive to communicate directly with you. We may send email marketing communications about Speakeasy. If you do not want to receive such email messages, you will be given the option to opt out. We will try to comply with your request(s) as soon as reasonably practical. Additionally, even after you opt out from receiving marketing messages from us, you will continue to receive administrative messages from us regarding our Services (e.g., account verification, purchase and billing confirmations and reminders, changes/updates to features of the Service, technical and security notices). In addition, you may opt out of allowing third-party online advertising networks to collect information from our Site by adjusting the browser settings on your computer or mobile device. Please refer to your mobile device or browser's technical information for instructions on how to delete and disable cookies, and other tracking tools. ## Protection of Personal Information Speakeasy cares about the security of your Personal Information, and we make reasonable efforts to ensure a level of security appropriate to the risk associated with the processing of your Personal Information. We maintain organizational, technical, and administrative procedures designed to protect your Personal Information against unauthorized access, deletion, loss, alteration, and misuse. Unfortunately, no data transmission or storage system can be guaranteed to be 100% secure. If you believe that your interaction with us is not secure, please contact us immediately. You are responsible for maintaining the secrecy of your unique password and account information, and for controlling access to your email communications from Speakeasy. Your privacy settings may also be affected by changes to the functionality of third-party sites and services that you add to the Speakeasy Service, such as single sign on. Speakeasy is not responsible for the functionality or security measures of any third party. Upon becoming aware of a breach of your Personal Information, we will notify you as quickly as we can and will provide timely information relating to the breach as it becomes known in accordance with any applicable laws and regulations or as is reasonably requested by you. ## Cookies We use cookies to remember information so that you don't have to re-enter it during your visit or the next time you visit the Site, to understand and save your preferences for future visits, to compile aggregate data about site traffic and site interaction, to provide custom, personalized content, support, or information, including advertising. Unlike persistent Cookies, session Cookies are deleted when you log off from the Services and close your browser. Although most browsers automatically accept Cookies, you can change your browser options to stop automatically accepting Cookies or to prompt you before accepting Cookies. Please note, however, that if you don't accept Cookies, you may not be able to access all portions or features of the Site or the Services. At present, there is no industry standard for recognizing Do Not Track browser signals, so we do not currently respond to them. ## Who at Speakeasy may access your Personal Information? Designated members of our staff may access Personal Information to help our customers with any questions they have, including help using our Services, investigating security issues, or following up on bug fixes with a customer. This activity is logged in our system for compliance, and we maintain different levels of access for its employees depending on their role in our company. ## Do we disclose any information to outside parties? Except as set out below, we do not sell, trade, or otherwise transfer to outside parties your Personal Information. - We may share your Personal Information with other companies owned by or under common ownership as Speakeasy, which also includes our subsidiaries (i.e., any organization we own or control). - These companies will use your Personal Information in the same way as we can under this Privacy Policy, unless otherwise specified. - We may disclose your Personal Information to third-party service providers (for example, payment processing and data storage and processing facilities) that we use to provide the Services. - We limit the Personal Information provided to these service providers to that which is reasonably necessary for them to perform their functions, and we require them to agree to maintain the confidentiality of such Personal Information. - We may contract with third-party service providers to assist us in better understanding our Site visitors. - These service providers are not permitted to use the information collected on our behalf except to help us conduct and improve our business. - We may also release your Personal Information when we believe release is appropriate to comply with the law, enforce our site policies, or protect our or others' rights, property, or safety. - In particular, we may release your Personal Information to third parties as required to (i) satisfy any applicable law, regulation, subpoena/court order, legal process or other government request, (ii) enforce our Terms of Service, including the investigation of potential violations thereof, (iii) investigate and defend ourselves against any third party claims or allegations, (iv) protect against harm to the rights, property or safety of Speakeasy, its users or the public as required or permitted by law and (v) detect, prevent or otherwise address criminal (including fraud or stalking), security or technical issues. - If you enable a public sharing of your Speakeasy applications, any information or content that you voluntarily disclose in your application becomes available to the public. If you remove information that you posted to the Services, copies may remain viewable in cached and archived pages of the Service, or if other users of the Services have copied or saved that information. - In the event that we enter into, or intend to enter into, a transaction that alters the structure of our business, such as a merger, reorganization, joint venture, assignment, sale, or change of ownership, we may share Personal Information for the purpose of facilitating and completing the transaction. ## How do we handle global transfers and processing of your Personal Information? Personal Information may be stored and processed in any country where we have operations, or where we engage service providers. This means that we may collect your Personal Information from, transfer it to, and store and process it in the United States and other countries outside of where you live. For example, some of our third-party providers may be located in different countries. Where this is the case, we will take steps to make sure the right security measures are taken so that your privacy rights continue to be protected as outlined in this Privacy Policy. By submitting your Personal Information, you're agreeing to this transfer, storing or processing. If you are located in the European Union or other regions with laws governing data collection and use that may differ from U.S. law, please note that we may transfer information, including Personal Information, to a country and jurisdiction that does not have the same data protection laws as your jurisdiction. If we transfer your Personal Information from the E.U. and process it in the United States, we do so in accordance with applicable law. In certain situations, we may be required to disclose personal information in response to lawful requests by public authorities, including to meet national security or law enforcement requirements. ## Retention of your Personal Information We retain your Personal Information for as long as we need to fulfill our Services. In addition, we retain Personal Information after we cease providing Services to you, to the extent necessary to comply with our legal obligations. Where we retain data, we do so in accordance with any limitation periods and records retention obligations that are imposed by applicable law. ## Third-party Links The Services may provide the ability to connect to other websites. These websites may operate independently from us and have their own privacy policies and notices, which we suggest you review. If the linked website is not owned or controlled by us, we are not responsible for its content, or the privacy practices ## Your Consent By using our site, you consent to this Privacy Policy. ## Minors These Services are not directed to individuals under the age of thirteen, and we kindly request they not provide any Personal Information through the Services. ## Your Rights Other rights you have include the rights to: - Ask for a copy of your Personal Information - Ask us to correct your Personal Information that is inaccurate, incomplete, or outdated - Ask us to transfer your Personal Information to other organizations. - Ask us to erase certain categories or types of information - If you choose to remove your Personal Information, you acknowledge that we may retain archived copies of your Personal Information in order to satisfy our legal obligations, or where we reasonably believe that we have a legitimate reason to do so. - Ask us to restrict certain processing - You have the right to object to processing of Personal Information. Where we have asked for your consent to process information, you have the right to withdraw this consent at any time. ## Changes to our Privacy Policy If you would like to submit a data rights request (also known as a data subject access request), or have any other questions, comments, or concerns about this privacy policy, please contact us at info@speakeasy.com or via mail at 90 New Montgomery Street Suite 700, San Francisco, CA 94105. ## Jurisdiction-specific Provisions - Residents of the European Economic Area and Switzerland: The Data Protection Officer can be contacted at [info@speakeasy.com](mailto:info@speakeasy.com). ## Contacting Us If you would like to submit a data rights request (also known as a data subject access request), or have any other questions, comments, or concerns about this privacy policy, please contact us using the following contact information: [info@speakeasy.com](mailto:info@speakeasy.com). # Security and data privacy Source: https://speakeasy.com/legal/product-security import { Callout, Table } from "@/mdx/components"; The Speakeasy platform is built with security and privacy as core development principles. Using company API specifications, the Speakeasy platform creates high-quality code hosted on GitHub. The following sections detail our privacy and security policy for all artifacts generated and maintained through Speakeasy, such as SDKs, as well as key information regarding security features like permissions and access. For our vulnerability disclosure policy, please visit [**this page**](/legal/security-policy). ## FAQ [Do I need to install something?](#do-i-need-to-install-something) [Does the Speakeasy platform access my API or customer data in any way?](#does-the-speakeasy-platform-access-my-api-or-customer-data-in-any-way) [What information about my company or users does Speakeasy have access to?](#what-information-about-my-company-or-users-does-speakeasy-have-access-to) [How does the Speakeasy service work?](#how-does-the-speakeasy-service-work) [Do I need to log in to the Speakeasy platform to use the service?](#do-i-need-to-log-in-to-the-speakeasy-platform-to-use-the-service) [Can Speakeasy be run in an air-gapped environment?](#can-speakeasy-be-run-in-an-air-gapped-environment) [Does Speakeasy store package manager secrets?](#does-speakeasy-store-package-manager-secrets) [What are Speakeasy's data storage policies?](#what-are-speakeasy-s-data-storage-policies) ### Do I need to install something? The [Speakeasy CLI](https://github.com/speakeasy-api/speakeasy) is a command-line interface (CLI) that facilitates the creation of SDKs, Terraform providers, Postman collections, and documentation. Written in Go and fully [open source](https://opensource.org), the CLI is compiled into binaries for easy customer use. Typically, the CLI is used within a customer's continuous integration/continuous deployment (CI/CD) workflow as part of the standard engineering and development flow. For most customers, this means installing the CLI as a GitHub Action in GitHub. If a customer doesn't use GitHub, the CLI can be installed on whichever system, server, or cloud environment the customer uses. ### Does the Speakeasy platform access my API or customer data in any way? Speakeasy does not sit in the API call chain. Therefore, the Speakeasy platform **does not have access to** or store your customer data or API request data in any form. ### What information about my company or users does Speakeasy have access to? Speakeasy has very little access to data about your employees and users. For user authorization purposes, Speakeasy stores user login email addresses. Speakeasy also stores limited service usage data, for example, the times at which an SDK generation is run. ### How does the Speakeasy service work? Speakeasy is shipped as a [verified GitHub Action](https://github.com/marketplace/actions/speakeasy-sdk-workflow-runner-action) and runs in your GitHub environment (either in the cloud or on-premises). The GitHub Action accesses your company's API specification, which is a static file describing the API contract, but this specification is not sent to Speakeasy. It's worth noting that this API specification is often made public and/or sent to third-party vendors to generate API documentation. ### Do I need to log in to the Speakeasy platform to use the service? Yes, using the Speakeasy platform requires you to log in through one of our supported authentication providers. However, this is only to request an API key (referred to in documentation as a `SPEAKEASY_API_KEY`). Once that key is obtained and stored, all features of the platform can be accessed directly via the CLI. ### Can Speakeasy be run in an air-gapped environment? Yes, the sending of usage metadata to Speakeasy can be disabled on request. Please reach out to [info@speakeasy.com](mailto:info@speakeasy.com) for more information. ### Does Speakeasy store package manager secrets? No, Speakeasy does not store any package manager secrets. Speakeasy uses these secrets to publish SDKs on your behalf. Package manager secrets are stored as secrets in your GitHub repository and are only viewable to members of your GitHub organization. Using Speakeasy to publish to package managers is optional. ### What are Speakeasy's data storage policies? **Speakeasy stores:** - Email addresses used to log in to the Speakeasy platform. - Metadata on SDK generation runs, specifically the date and time of SDK generation, the language, the specification version, and error details for any errors that occurred. - Point-in-time snapshots of API specifications for comparing changes to an API specification over time. **Speakeasy DOES NOT store:** - Any customer-generated code, unless specifically configured in a Speakeasy-hosted repository. ## Customer-hosted repositories The following guidance refers to artifacts hosted in the Speakeasy GitHub organization, `speakeasy-sdks`, on behalf of the customer. A Speakeasy-created artifact (like an SDK) can be hosted on GitHub in a repository in your GitHub organization (for example, `www.github.com/yourcompany/sdk`). The Speakeasy service is provided through the Speakeasy CLI, which is distributed as a Go binary and accessible through various package managers like Homebrew and Chocolatey. Speakeasy generates code in one of two ways: 1. Locally, using the Speakeasy CLI on a developer's machine. 2. On infrastructure connected to your organization's GitHub account, using "GitHub runners". Artifacts created in either of these ways will require certain permissions to be granted to Speakeasy workflows in your GitHub repository. These permissions are self-documenting in GitHub workflow files, as illustrated [here](https://github.com/speakeasy-sdks/template-sdk/blob/main/.github/workflows/speakeasy_sdk_generation.yml). The following snippet is from the GitHub workflow file that Speakeasy creates and maintains in your SDK repository: ```yaml permissions: checks: write contents: write pull-requests: write statuses: write ``` Here, Speakeasy requests `write` permission on `checks`, `contents`, `pull-requests`, and `statuses` in your repository. Speakeasy will respect any permissions inherited from the top-level settings of the GitHub organization. ## Speakeasy-hosted repositories The following guidance refers to artifacts hosted on behalf of the customer in the Speakeasy GitHub organization, `speakeasy-sdks`. Speakeasy-hosted artifacts are created in the [`speakeasy-sdks` GitHub organization](https://github.com/speakeasy-sdks) owned by Speakeasy. These artifacts follow the same set of security guidelines and permissions as customer-hosted artifacts. ## Code security and privacy ### CLI events The Speakeasy CLI submits events to the Speakeasy platform to monitor errors, usage, and other telemetry data. This data is used to track and resolve issues, identify trends, and improve the Speakeasy platform. The CLI commands that currently send telemetry data are `speakeasy run` and `speakeasy generate`. The data points these commands send are as follows:
### Third-party dependencies - **Third-party code dependencies:** All Speakeasy-created SDKs use minimal to no third-party dependencies. Please see the [language-specific design pages](/docs/languages/philosophy) for more information. - **All tokens stored as GitHub secrets:** Publishing tokens, such as those used for npm or PyPI, are stored as [GitHub Actions secrets](https://docs.github.com/en/rest/actions/secrets). The Speakeasy GitHub workflows use these tokens to publish SDK packages to package managers on behalf of the customer, but will never export or have plain-text access to these tokens. ### Code ownership - All code generated by Speakeasy is owned by the customer. Speakeasy licenses code with the open source [MIT License](https://opensource.org/license/mit/) by default. The license can be altered by the owner of the SDK at any time after generation. - **Authentication with the Speakeasy platform:** When the Speakeasy code generator is invoked, the generation is authenticated with the Speakeasy platform using a GitHub secret named `SPEAKEASY_API_KEY`. This is an opaque token that authenticates each generation run with a workspace on the platform, enabling Speakeasy to collect metadata on generations on a per-customer basis. This metadata does not include generated code or the raw API specification. ## Found a bug or vulnerability? Think you may have found a security bug? We'd be happy to work with you to explore and resolve the issue - and ensure you are fairly rewarded. Rewards will be based on severity, as per the [Common Vulnerability Scoring System](https://docs.hackerone.com/hackers/severity.html?). Get in touch with us at [bugs@speakeasy.com](mailto:bugs@speakeasy.com) to learn more. ## Questions? Please don't hesitate to reach out to us at [info@speakeasy.com](mailto:info@speakeasy.com) with any questions you have about the information contained on this page. # Introduction Source: https://speakeasy.com/legal/security-policy This vulnerability disclosure policy applies to any vulnerabilities you are considering reporting to Speakeasy so long as the website has a published security.txt file that references this policy. We recommend reading this vulnerability disclosure policy fully before you report a vulnerability and always acting in compliance with it. We value those who take the time and effort to report security vulnerabilities according to this policy. However, we do not offer monetary rewards for vulnerability disclosures. # Reporting If you believe you have found a security vulnerability relating to the Organisation’s system, please submit a vulnerability report to the address defined in [**security@speakeasy.com**](mailto:security@speakeasy.com). In your report please include details of: - The website, IP or page where the vulnerability can be observed. - A brief description of the type of vulnerability, for example; “XSS vulnerability”. - Steps to reproduce. These should be a benign, non-destructive, proof of concept. This helps to ensure that the report can be triaged quickly and accurately. It also reduces the likelihood of duplicate reports, or malicious exploitation of some vulnerabilities, such as sub-domain takeovers. # What to expect After you have submitted your report, we will respond to your report within 5 working days and aim to triage your report within 10 working days. We’ll also aim to keep you informed of our progress. Priority for remediation is assessed by looking at the impact, severity and exploit complexity. Vulnerability reports might take some time to triage or address. You are welcome to enquire on the status but should avoid doing so more than once every 14 days. This allows our teams to focus on the remediation. We will notify you when the reported vulnerability is remediated, and you may be invited to confirm that the solution covers the vulnerability adequately. Once your vulnerability has been resolved, we welcome requests to disclose your report. We’d like to unify our guidance, so please do continue to coordinate public release with us. # Guidance You must NOT: - Break any applicable law or regulations. - Access unnecessary, excessive or significant amounts of data. - Modify data in the Organisation's systems or services. - Use high-intensity invasive or destructive scanning tools to find vulnerabilities. - Attempt or report any form of denial of service, e.g. overwhelming a service with a high volume of requests. - Disrupt the Organisation's services or systems. - Submit reports detailing non-exploitable vulnerabilities, or reports indicating that the services do not fully align with “best practice”, for example missing security headers. - Submit reports detailing TLS configuration weaknesses, for example “weak” cipher suite support or the presence of TLS1.0 support. - Communicate any vulnerabilities or associated details other than by means described in the published security.txt. - Social engineer, ‘phish’ or physically attack the Organisation's staff or infrastructure. - Demand financial compensation in order to disclose any vulnerabilities. You must: - Always comply with data protection rules and must not violate the privacy of any data the Organisation holds. You must not, for example, share, redistribute or fail to properly secure data retrieved from the systems or services. - Securely delete all data retrieved during your research as soon as it is no longer required or within 1 month of the vulnerability being resolved, whichever occurs first (or as otherwise required by data protection law). # Legalities This policy is designed to be compatible with common vulnerability disclosure good practice. It does not give you permission to act in any manner that is inconsistent with the law, or which might cause the Organization or partner organisations to be in breach of any legal obligations. # terms-of-service Source: https://speakeasy.com/legal/terms-of-service

Speakeasy Development, Inc.

Welcome to Speakeasy Development, Inc. ("Speakeasy"), herein referred to as "Company," "we," "us," or "our". By accessing and using our services ("Services"), which include but are not limited to our websites at [https://www.speakeasy.com](https://www.speakeasy.com) and [https://getgram.ai](https://getgram.ai), Command Line Interface (CLI) tool, GitHub Action, Web User Interface (Web UI), and any related software, applications, tools, or utilities provided by the Company, you, the user, agree to be bound by the following terms of service ("Terms"). These Terms form a legally binding contract between you and the Company. Acceptance of Terms: By using any part of our Services, clicking on the "I Agree" or similar button, installing our software, or otherwise engaging with our Services, you acknowledge that you have read, understood, and agree to these Terms. If you are accepting these Terms on behalf of an employer or another legal entity, you represent and warrant that you have the authority to bind that entity to these Terms. If you do not agree with any part of these Terms, you must not use the Services. Separate Agreements: If a separate written Services Agreement exists between a customer (or entity) and the Company regarding the Services, the terms of that agreement shall take precedence over these Terms. Use of Services: Use of the Services signifies agreement to be bound by these Terms and any modifications made to them. The Company reserves the right to modify these Terms at any time, with changes becoming effective upon posting to the website or direct communication. Continued use of the Services following such changes constitutes acceptance of the new Terms. Eligibility: By using our Services, you confirm that you are legally capable of entering into binding contracts and that your use of the Services is not prohibited by any applicable laws or agreements. ## 1. Services and Support 1.1. Subject to the terms of this Agreement, Company will use commercially reasonable efforts to provide Customer the Services. Services include but are not limited to our SDK generation tools, documentation services, hosted infrastructure services, and any related software, applications, tools, or utilities. 1.2 Subject to the terms hereof, Company will provide Customer with reasonable technical support services in accordance with the Company's standard practice ## 2. Restrictions and Responsibilities 2.1 Customer will not, directly or indirectly: reverse engineer, decompile, disassemble or otherwise attempt to discover the source code, object code or underlying structure, ideas, know-how or algorithms relevant to the Services or any software, documentation or data related to the Services (“Software”); modify, translate, or create derivative works based on the Services or any Software (except to the extent expressly permitted by Company or authorized within the Services); use the Services or any Software for timesharing or service bureau purposes or otherwise for the benefit of a third; or remove any proprietary notices or labels. With respect to any Services provided on a cloud-hosted basis, where Company (or its third-party service provider) hosts the Services, Company grants to Customer a non-exclusive, non-transferable, non-sublicensable right to access and use the Services on a software-as-a-service basis for Customer's internal business purposes. With respect to any Software that is distributed or provided to Customer for use on Customer premises or devices, Company hereby grants Customer a non-exclusive, non-transferable, non-sublicensable license to use such Software during the Term only in connection with the Services. Note that any client SDKs produced during the Term may continue to be used even after the Term expires. With respect to any Services that are self-hosted by the Customer, Company grants Customer a non-exclusive, non-transferable, non-sublicensable right to (i) download, install and use any executable Software provided by Company for the purpose of integrating the Services into the Customer Environment (as defined below), and (ii) access and use the Services solely to enable Customer to connect and integrate the Service with Customer's servers and/or cloud-hosting environments (which may include, without limitation, any of the foregoing that are made available to Customer by a third-party service provider) (collectively, “Customer Environment”). Customer will be solely responsible for integrating and implementing the Services with the Customer Environment. Customer will only permit the Services to be accessed by Customer's employees or contractors that are authorized by Customer to access the Services, provided that Customer shall remain liable for all acts or omissions of such users. 2.2 Further, Customer may not remove or export from the United States or allow the export or re-export of the Services, Software or anything related thereto, or any direct product thereof in violation of any restrictions, laws or regulations of the United States Department of Commerce, the United States Department of Treasury Office of Foreign Assets Control, or any other United States or foreign agency or authority. As defined in FAR section 2.101, the Software and documentation are “commercial items” and according to DFAR section 252.227 7014(a)(1) and (5) are deemed to be “commercial computer software” and “commercial computer software documentation.” Consistent with DFAR section 227.7202 and FAR section 12.212, any use modification, reproduction, release, performance, display, or disclosure of such commercial software or commercial software documentation by the U.S. Government will be governed solely by the terms of this Agreement and will be prohibited except to the extent expressly permitted by the terms of this Agreement. 2.3 Customer represents, covenants, and warrants that Customer will use the Services only in compliance with the terms of this Agreement and all applicable laws and regulations. Customer hereby agrees to indemnify and hold harmless Company against any damages, losses, liabilities, settlements and expenses (including without limitation costs and attorneys' fees) in connection with any claim or action that arises from an alleged violation of the foregoing or otherwise from Customer's use of Services. Although Company has no obligation to monitor Customer's use of the Services, Company may do so and may prohibit any use of the Services it believes may be (or alleged to be) in violation of the foregoing. 2.4 Customer shall be responsible for obtaining and maintaining any equipment and ancillary services needed to connect to, access or otherwise use the Services, including, without limitation, modems, hardware, servers, software, operating systems, networking, web servers and the like (collectively, “Equipment”). Customer shall also be responsible for maintaining the security of the Equipment, Customer account, passwords (including but not limited to administrative and user passwords) and files, and for all uses of Customer account or the Equipment with or without Customer's knowledge or consent. ## 3. Confidentiality; Proprietary Rights 3.1 Each party (the "Receiving Party") understands that the other party (the "Disclosing Party") has disclosed or may disclose business, technical or financial information relating to the Disclosing Party's business (hereinafter referred to as "Proprietary Information" of the Disclosing Party). Proprietary Information of Company includes non-public information regarding features, functionality and performance of the Service. Proprietary Information of Customer includes non-public data provided by Customer to Company to enable the provision of the Services, as well as data processed through Company's hosted infrastructure services on Customer's behalf ("Customer Data"). The Receiving Party agrees: (i) to take reasonable precautions to protect such Proprietary Information, and (ii) not to use (except in performance of the Services or as otherwise permitted herein) or divulge to any third person any such Proprietary Information. The Disclosing Party agrees that the foregoing shall not apply with respect to any information after five (5) years following the disclosure thereof or any information that the Receiving Party can document (a) is or becomes generally available to the public, or (b) was in its possession or known by it prior to receipt from the Disclosing Party, or (c) was rightfully disclosed to it without restriction by a third party, or (d) was independently developed without use of any Proprietary Information of the Disclosing Party or (e) is required to be disclosed by law. 3.2 Customer shall own all right, title and interest in and to the Customer Data and any SDKs, Terraform providers, and documentation generated for Customer using Company's Services. Company shall own and retain all right, title and interest in and to (a) the Services and Software (including Company's SDK, Terraform provider, and documentation generators), and all improvements, enhancements or modifications thereto, (b) any software, applications, inventions or other technology developed in connection with Services or support (if applicable), and (c) all intellectual property rights related to any of the foregoing. 3.3 Notwithstanding anything to the contrary, Company shall have the right to collect and analyze data and other information relating to the provision, use and performance of various aspects of the Services and related systems and technologies (including, without limitation, information concerning Customer Data and data derived therefrom), and Company will be free (during and after the term hereof) to (i) use such information and data to improve and enhance the Services and for other development, diagnostic and corrective purposes in connection with the Services and other Company offerings, and (ii) disclose such data solely in aggregate or other de-identified form in connection with its business, provided that such collection and use shall be subject to the terms of any applicable Data Processing Agreement between the parties. No rights or licenses are granted except as expressly set forth herein. 3.4 Data Processing: For Services involving the processing of personal data, Company acts as a data processor in accordance with applicable data protection laws. If Customer is subject to the GDPR, UK GDPR, or similar data protection laws, the Company’s Data Processing Addendum (“DPA”), available at https://trust.speakeasy.com/, is hereby incorporated into and made a part of these Terms and applies automatically to the Processing of Personal Data. ## 4 Payment of Fees Speakeasy offers a variety of service plans, from complimentary access to extended features available through paid subscriptions. Detailed information on what each plan includes can be found on our pricing page. Access to paid plans is facilitated securely through our hosted payment link, and by selecting any of these plans, you agree to the payment terms outlined herein, managed via our chosen third-party payment processor ("PSP"). 4.1 Subscription and Billing Cycle: Payment for your chosen service plan is required in advance and will be billed by the PSP on or shortly after the subscription initiation date and subsequently at the beginning of each billing cycle (monthly, annually, or as otherwise specified). The fees charged are non-refundable, except as explicitly stated in these Terms. 4.2 Adjustments to Service Plans: Users may upgrade or downgrade their service plans. Upgrades take effect immediately, and additional charges will be applied on a pro-rata basis. Downgrades, including transitions from paid to freemium plans, take effect at the start of the next billing cycle. 4.3 Changes to Fees: Speakeasy reserves the right to change fees or billing methods for service plans at any time. Users will be notified of any fee changes in advance. Disagreement with the changes requires cancellation of your subscription. Continued use after the change takes effect implies acceptance of the new fees. 4.4 Responsibility for Taxes and Charges: Users are responsible for any taxes, duties, or charges incurred in connection with the services, except for those based on Speakeasy's income. All listed prices do not include such taxes or charges. 4.5 Payment Information: You agree to provide accurate and up-to-date payment information to the PSP and to promptly update your information with any changes. Failure to process a payment will result in suspension of the Services until payment can be collected. You authorize us to continue billing the provided payment method for all charges due and to update information from your payment provider to continue billing if necessary. ## 5. Term and Termination 5.1 Term: This Agreement becomes effective upon your first use of the Services and remains in effect until terminated by either party according to the terms provided herein. The term of this Agreement consists of the following: - Initial Term: For all users, the "initial term" begins upon the first use of the Services. For users who choose a paid subscription, this term extends through the end of the first billing cycle specified at the time of purchase (e.g., one month, one year). - Renewal Term: After the initial term, the Agreement will automatically renew for additional periods matching the length of the initial paid subscription cycle (e.g., monthly, annually). For users on freemium or trial plans who have not transitioned to a paid subscription, the Agreement will continue indefinitely on the same terms until either party opts for termination. - Termination: Either party may terminate this Agreement with at least thirty (30) days' notice prior to the end of the current term, whether it be the initial term or any renewal term. 5.2 Termination for Cause: Either party may terminate this Agreement with thirty (30) days' written notice if the other party materially breaches any terms or conditions and fails to cure such breach within the thirty (30) day notice period. Immediate termination is allowed in cases of nonpayment. 5.3 Effects of Termination: Upon termination, you must cease all use of the Services, and we will disable your access. We will provide you with access to retrieve your data for thirty (30) days post-termination. After this period, we may delete your data, although we are not obligated to do so. 5.4 Survival: Provisions regarding payment, data ownership, confidentiality, liability limitations, and any other terms which by their nature should survive, will remain in effect after this Agreement ends. ## 6. Warranty and Disclaimer Company shall use reasonable efforts consistent with prevailing industry standards to maintain the Services in a manner which minimizes errors and interruptions in the Services and shall perform the Implementation Services (if applicable) in a professional and workmanlike manner. Services may be temporarily unavailable for scheduled maintenance or for unscheduled emergency maintenance, either by Company or by third-party providers, or because of other causes beyond Company's reasonable control, but Company shall use reasonable efforts to provide advance notice in writing or by email of any scheduled service disruption. HOWEVER, COMPANY DOES NOT WARRANT THAT THE SERVICES WILL BE UNINTERRUPTED OR ERROR FREE; NOR DOES IT MAKE ANY WARRANTY AS TO THE RESULTS THAT MAY BE OBTAINED FROM USE OF THE SERVICES. EXCEPT AS EXPRESSLY SET FORTH IN THIS SECTION, THE SERVICES AND IMPLEMENTATION SERVICES ARE PROVIDED “AS IS” AND COMPANY DISCLAIMS ALL WARRANTIES, EXPRESS OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. ## 7. Limitation of Liability NOTWITHSTANDING ANYTHING TO THE CONTRARY, EXCEPT FOR BODILY INJURY OF A PERSON, COMPANY AND ITS SUPPLIERS (INCLUDING BUT NOT LIMITED TO ALL EQUIPMENT AND TECHNOLOGY SUPPLIERS), OFFICERS, AFFILIATES, REPRESENTATIVES, CONTRACTORS AND EMPLOYEES SHALL NOT BE RESPONSIBLE OR LIABLE WITH RESPECT TO ANY SUBJECT MATTER OF THIS AGREEMENT OR TERMS AND CONDITIONS RELATED THERETO UNDER ANY CONTRACT, NEGLIGENCE, STRICT LIABILITY OR OTHER THEORY: (A) FOR ERROR OR INTERRUPTION OF USE OR FOR LOSS OR INACCURACY OR CORRUPTION OF DATA OR COST OF PROCUREMENT OF SUBSTITUTE GOODS, SERVICES OR TECHNOLOGY OR LOSS OF BUSINESS; (B) FOR ANY INDIRECT, EXEMPLARY, INCIDENTAL, SPECIAL OR CONSEQUENTIAL DAMAGES; (C) FOR ANY MATTER BEYOND COMPANY'S REASONABLE CONTROL; OR (D) FOR ANY AMOUNTS THAT, TOGETHER WITH AMOUNTS ASSOCIATED WITH ALL OTHER CLAIMS, EXCEED THE FEES PAID BY CUSTOMER TO COMPANY FOR THE SERVICES UNDER THIS AGREEMENT IN THE 12 MONTHS PRIOR TO THE ACT THAT GAVE RISE TO THE LIABILITY, IN EACH CASE, WHETHER OR NOT COMPANY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. ## 8. Miscellaneous If any provision of this Agreement is found to be unenforceable or invalid, that provision will be limited or eliminated to the minimum extent necessary so that this Agreement will otherwise remain in full force and effect and enforceable. This Agreement is not assignable, transferable or sublicensable by Customer except with Company's prior written consent. Company may transfer and assign any of its rights and obligations under this Agreement without consent. Company may use Customer's name and logo on Company's website and in marketing materials to identify Customer as a client of Company. This Agreement is the complete and exclusive statement of the mutual understanding of the parties and supersedes and cancels all previous written and oral agreements, communications and other understandings relating to the subject matter of this Agreement, and that all waivers and modifications must be in a writing signed by both parties, except as otherwise provided herein. No agency, partnership, joint venture, or employment is created as a result of this Agreement and Customer does not have any authority of any kind to bind Company in any respect whatsoever. In any action or proceeding to enforce rights under this Agreement, the prevailing party will be entitled to recover costs and attorneys' fees. All notices under this Agreement will be in writing and will be deemed to have been duly given when received, if personally delivered; when receipt is electronically confirmed, if transmitted by facsimile or email; the day after it is sent, if sent for next day delivery by recognized overnight delivery service; and upon receipt, if sent by certified or registered mail, return receipt requested. This Agreement shall be governed by the laws of the State of California without regard to its conflict of laws provisions. Customer agrees to reasonably cooperate with Company to serve as a reference account upon request. ## 9. Contact Information If you have any questions about these Terms or the Services please contact Speakeasy at [info@speakeasy.com](mailto:info@speakeasy.com). # What is MCP? An overview of the Model Context Protocol Source: https://speakeasy.com/mcp/core-concepts The AI ecosystem has evolved so much that today's LLMs are no longer just completion engines. Coupled with an interface, LLMs can become clients capable of taking actions, querying systems, and interpreting structured content. In modern architectures, LLMs often operate alongside databases, APIs, or custom functions. To support this, developers frequently inject external data directly into their prompts. The simplest way to do this is to pull data from a service and pass it as plain text to the model: ```python import requests from openai import OpenAI client = OpenAI() # Step 1: Get real-world data response = requests.get("https://api.weatherapi.com/v1/current.json?q=Paris&key=demo") weather = response.json() # Step 2: Inject it into a prompt manually prompt = f""" You are a weather assistant. Here's the current weather in Paris: Temperature: {weather['current']['temp_c']}°C Condition: {weather['current']['condition']['text']} Write a friendly weather summary. """ # Step 3: Call the model completion = client.chat.completions.create( model="gpt-4o", messages=[ {"role": "user", "content": prompt} ], ) ``` This works, but only on a small scale. The model has to interpret the meaning of the data from plain text, which bloats token usage and loses structure. To minimize token use and loss of structure, native toolchains emerged. Using toolchains such as OpenAI function calling, Anthropic tools, and Mistral functions, you can define structured input and output schemas, register tools, and let the model **call** them in a controlled way. ```python from openai import OpenAI client = OpenAI() tools = [{ "type": "function", "function": { "name": "query_user_count", "description": "Query the database to get the total number of users.", "parameters": { "type": "object", "properties": { "table": { "type": "string", "description": "Name of the table to query, e.g. 'users'" } }, "required": ["table"], "additionalProperties": False } } }] response = client.chat.completions.create( model="gpt-4o", messages=[ { "role": "user", "content": "How many users are currently in the system?" } ], tools=tools, tool_choice="auto" ) # Output the tool call generated by the model print(response.choices[0].message.tool_calls) ``` However, using these toolchains comes with some issues: - You are limited to a single model provider. - Prompts also remain brittle, undocumented, and tied to proprietary formats. This is where the Model Context Protocol (MCP) comes in. ## What is MCP? MCP is a low-level JSON-RPC protocol originally proposed by Anthropic. It standardizes communication between LLMs and real-world environments. At its core, MCP defines two roles: the server and the client. The **MCP server** is the backend that exposes three primitives: - Tools: Functions that perform actions (such as writing a file or placing an order) - Resources: Read-only, URI-addressable context (for example, logs, config, and APIs) - Prompts: Reusable message templates that guide interactions with the MCP server The **MCP client** is the component that understands the protocol and communicates with the MCP server by sending the following requests and interpreting the server's structured responses: - `tools/call` - `resources/read` - `prompts/get` Here's where confusion often arises: Who actually talks to the MCP server? Is it the LLM or the agent? The answer is that neither the LLM nor the agent talks directly to the MCP server. The MCP client acts as the middle layer between the server and the model. It may be embedded in: - A desktop application (such as Claude Desktop) - An agent runtime - A custom LLM client These systems use the MCP client to interact with the MCP server, but they are still responsible for integrating results into LLM prompts or workflows. For example, in an agentic architecture, the flow looks like this: 1. The user inputs, _"How many users signed up this month?"_ 2. The agent parses the request and calls a tool. 3. The agent's embedded MCP client sends a `tools/call` request to the MCP server. 4. The MCP server executes the tool function, which makes a database query and returns a result. The MCP server returns the result with a structured response to the MCP client. 5. The agent takes that response and incorporates it into the next LLM prompt or uses it to make a decision. ## How does MCP differ from APIs and tool calling? Unlike many client-server protocols where clients only make requests and servers only respond, MCP supports full bidirectional communication. - **Client-to-server**: The client can request resources, call tools, and get prompts. - **Server-to-client**: The server can request LLM sampling, ask for root listings, and send notifications. - **Notifications**: Both sides can send one-way messages without expecting responses. This bidirectional design enables workflows in which servers can actively participate in decision-making by requesting LLM assistance through the client. ![MCP Architecture](/assets/mcp/getting-started/mcp-architecture.png) MCP offers SDKs in multiple languages (including Python and JavaScript) to simplify building and exposing MCP-compliant servers. ## MCP Server Example Here is an example of an MCP server in Python that exposes a single tool to write notes to a local directory: ```python filename="mcp-server.py" from mcp.server.fastmcp import FastMCP from mcp.server.stdio import stdio_server from mcp.server import InitializationOptions, NotificationOptions from pathlib import Path import asyncio NOTES_DIR = Path("notes") NOTES_DIR.mkdir(exist_ok=True) app = FastMCP("MCP Notes Server") #### TOOLS #### @app.tool() def write_note(slug: str, content: str) -> str: note_path = NOTES_DIR / f"{slug}.txt" note_path.write_text(content.strip(), encoding="utf-8") return f"Note '{slug}' saved." #### RESOURCES #### @app.resource("note://{slug}") def read_note(slug: str) -> str: note_path = NOTES_DIR / f"{slug}.txt" if not note_path.exists(): return "Note not found." return note_path.read_text(encoding="utf-8") #### PROMPTS #### @app.prompt() def suggest_note_prompt(topic: str) -> str: return f""" Write a short, thoughtful note for someone who needs advice about: {topic} """ #### ROOTS #### @app.tool() def list_notes(root: str = None) -> list[str]: all_slugs = [f"note://{f.stem}" for f in NOTES_DIR.glob("*.txt")] if root: return [slug for slug in all_slugs if slug.startswith(root)] return all_slugs # ----------------------------- # ENTRYPOINT (Explicit Transport) # ----------------------------- async def main(): async with stdio_server() as (read_stream, write_stream): await app.run( read_stream, write_stream, InitializationOptions( server_name="mcp-notes-server", server_version="0.1.0", capabilities=app.get_capabilities( notification_options=NotificationOptions(), experimental_capabilities={}, ), ), ) if __name__ == "__main__": asyncio.run(main()) ``` Let's break it down into chunks: ### Tools A tool is a function exposed by the server that an AI agent can call using `JSON-RPC`. Each tool includes its name, input parameters, output schema, and a description. ```python #### TOOLS #### @app.tool() def write_note(slug: str, content: str) -> str: note_path = NOTES_DIR / f"{slug}.txt" note_path.write_text(content.strip(), encoding="utf-8") return f"Note '{slug}' saved." ``` ### Resources Resources are named, read-only data references addressable via URIs. Resources are used to load contextual read-only data into the MCP session. ```python #### RESOURCES #### @app.resource("note://{slug}") def read_note(slug: str) -> str: note_path = NOTES_DIR / f"{slug}.txt" if not note_path.exists(): return "Note not found." return note_path.read_text(encoding="utf-8") ``` ### Prompts Prompts are predefined message templates that servers expose to clients. When invoked with arguments, they return a list of messages (`messages[]`) that the client sends to the LLM to perform a specific task. ```python #### PROMPTS #### @app.prompt() def suggest_note_prompt(topic: str) -> str: return f""" Write a short, thoughtful note for someone who needs advice about: {topic} """ ``` ### Roots Roots are a set of scoped access boundaries provided by the client at handshake. Roots allow the server to limit the visibility of tools, resources, or data based on a given namespace. ```python #### ROOTS #### @app.tool() def list_notes(root: str = None) -> list[str]: all_slugs = [f"note://{f.stem}" for f in NOTES_DIR.glob("*.txt")] if root: return [slug for slug in all_slugs if slug.startswith(root)] return all_slugs ``` ### Transports A transport defines the communication method the server uses to handle client connections. MCP supports multiple transports, including `stdio` for local execution and `http` for web-based deployment. ```python async def main(): async with stdio_server() as (read_stream, write_stream): await app.run( read_stream, write_stream, InitializationOptions( server_name="mcp-notes-server", server_version="0.1.0", capabilities=app.get_capabilities( notification_options=NotificationOptions(), experimental_capabilities={}, ), ), ) if __name__ == "__main__": asyncio.run(main()) ``` # Completables: Autocomplete for MCP arguments Source: https://speakeasy.com/mcp/core-concepts/completables Completables are an advanced MCP feature that provides autocomplete functionality for prompt and resource arguments. When you register a prompt or resource that requires user input, you can make the arguments **completable**, allowing the MCP client to provide suggestions by querying the server for available options. This feature has not been widely adopted yet, but the TypeScript SDK supports it. Currently, it's primarily implemented in the `@modelcontextprotocol/inspector` client. ## How completables work When you register a completable argument, the MCP client can send partial input to the server and receive a list of matching suggestions. This is useful for arguments like: - Chat or conversation names - File paths - User names - Project identifiers - Any other values that can be looked up or filtered ## Implementing completables Here's how to register a completable for a prompt that requires a chat name: ```typescript import { Completable } from "@modelcontextprotocol/sdk/server/completable.js"; mcpServer.prompt( "whatsapp_chat_summarizer", "Summarize WhatsApp chat and provide insights", { chatName: Completable.create(z.string(), { complete: async (partial) => { return await chatService.filterChatsBySubstring(partial); }, }).describe("Name of the WhatsApp chat to summarize"), }, async (args) => { const { chatName = "" } = args; // Find the chat by name const targetChat = await chatService.findChatByName(chatName); // Get recent messages for analysis const messages = await messageService.getMessages(targetChat.id); const promptText = `Analyze this WhatsApp chat data for insights: Chat Information: - Chat Name: ${targetChat.name} - Chat Type: ${targetChat.isGroup ? "Group Chat" : "Individual Chat"} Recent Messages (${messages.length} messages): ${messages.map((msg) => msg._serializedContent).join("\n")} Please provide a detailed summary.`; return { description: `Summary of WhatsApp chat: ${targetChat.name}`, messages: [ { role: "user", content: { type: "text", text: promptText, }, }, ], }; }, ); ``` ## The autocomplete flow When a user types into an argument field: 1. The MCP client sends a `completion/complete` request with the partial input 2. The MCP server calls the `complete` function with the partial string 3. The server returns matching suggestions 4. The client displays the suggestions to the user 5. The user selects a suggestion or continues typing ## Use cases for completables Completables are particularly valuable for: - **Large datasets**: When there are too many options to display in a dropdown - **Fuzzy matching**: When users might not know the exact spelling - **Dynamic data**: When the available options change based on external state - **Performance**: When loading all options upfront would be slow ## Current limitations While completables are part of the MCP specification and supported by the TypeScript SDK, client support is limited. Most MCP clients don't yet implement the `completion/complete` request handling required for this feature to work. As the MCP ecosystem matures, we expect to see broader adoption of completables across different clients, making them a more practical option for improving the user experience of argument-heavy prompts and resources. # What are MCP prompts? Source: https://speakeasy.com/mcp/core-concepts/prompts MCP prompts are reusable, structured message templates exposed by MCP servers to guide interactions with agents. Unlike tools (which execute logic) or resources (which provide read-only data), prompts return a predefined list of messages meant to initiate consistent model behavior. Prompts are declarative, composable, and designed for user-initiated workflows, such as: - Slash commands or quick actions triggered via UI - Task-specific interactions, like summarization or code explanation You can use prompts when you want to define how users engage with the model but not to perform logic or to serve contextual data. ## Prompt structure A prompt is a named, parameterized template. It defines: - A `name` (a unique identifier) - An optional `description` - An optional list of structured `arguments` ```json { "name": "summarize-errors", "description": "Summarize recent error logs", "arguments": [ { "name": "logUri", "description": "URI of the log resource", "required": true } ] } ``` The server exposes prompts via `prompts/list` and provides message content on `prompts/get`. ### Discovering prompts Clients use `prompts/list` to fetch available prompt definitions: ```json { "method": "prompts/list" } ``` The response includes a list of prompts: ```json { "prompts": [ { "name": "explain-code", "description": "Explain how a function works", "arguments": [{ "name": "code", "required": true }] } ] } ``` ### Using prompts To use a prompt, clients call `prompts/get` with a prompt `name` and `arguments`: ```json { "method": "prompts/get", "params": { "name": "explain-code", "arguments": { "code": "def hello(): print('hi')" } } } ``` The server responds with a `messages[]` array, ready to send to the model: ```json { "description": "Explain how a function works", "messages": [ { "role": "user", "content": { "type": "text", "text": "Explain this Python code:\n\ndef hello(): print('hi')" } } ] } ``` ## Defining and serving prompts in Python The following example defines a simple MCP prompt called `git-commit` that helps users generate commit messages from change descriptions. ```python from mcp.server import Server, stdio import mcp.types as types import asyncio app = Server("git-prompts-server") @app.list_prompts() async def list_prompts() -> list[types.Prompt]: return [ types.Prompt( name="git-commit", description="Generate a Git commit message from a code diff or change summary", arguments=[ types.PromptArgument( name="changes", description="Code diff or explanation of the changes made", required=True ) ] ) ] @app.get_prompt() async def get_prompt(name: str, arguments: dict[str, str]) -> types.GetPromptResult: if name != "git-commit": raise ValueError("Unknown prompt") changes = arguments.get("changes", "") return types.GetPromptResult( messages=[ types.PromptMessage( role="user", content=types.TextContent( type="text", text=( "Generate a Git commit message summarizing these changes:\n\n" f"{changes}" ) ) ) ] ) ``` In this example, we: - **Register a static prompt** named `git-commit` with a human-readable description and a required `changes` argument. - **Expose metadata via `@list_prompts`** so UIs and clients can discover the prompt. - **Implement prompt generation via `@get_prompt`**, which creates a single message that asks the agent to produce a commit message based on input. - **Avoid side effects**, as the server does not evaluate or format the response but it does structure a message. ## Best practices and pitfalls to avoid Here are some best practices for implementing MCP prompts: - Use clear, actionable names (for example, `summarize-errors`, not `get-summarized-error-log-output`). - Validate all required arguments up front. - Keep prompts deterministic and stateless (using the same input should produce the same output). - Embed resources directly, if needed, for model context. - Provide concise descriptions to improve UI discoverability. When implementing MCP prompts, avoid the following common mistakes: - Allowing missing or malformed arguments - Using vague or overly long prompt names - Passing oversized inputs (such as full files or large diffs) - Failing to sanitize non-UTF-8 or injection-prone strings ### Prompts vs tools vs resources The table below compares the three core primitives in MCP: | Feature | Prompts | Tools | Resources | | ---------------- | ------------------------------------- | ---------------------------------- | ---------------------------------- | | **Purpose** | Guide model interaction | Execute logic with side effects | Provide structured read-only data | | **Triggered by** | User or UI | Agent or client (`tools/call`) | Agent or client (`resources/read`) | | **Behavior** | Returns `messages[]` | Runs a function; returns a result | Returns static or dynamic content | | **Side effects** | None | Yes (I/O, API calls, mutations) | None | | **Composition** | Can embed arguments and resources | Accepts structured input | URI-scoped, optionally templated | | **Use cases** | Summarization, Q&A, message templates | File writing, API calls, workflows | Logs, config files, external data | ## Practical implementation example MCP prompts are a powerful way to define reusable templates that combine context from your application with instructions for the LLM. Here's how to implement a prompt using the TypeScript SDK. This example creates a WhatsApp chat summarization prompt that retrieves chat data and formats it for the LLM: ```typescript mcpServer.prompt( "whatsapp_chat_summarizer", "Summarize WhatsApp chat and provide insights", { chatName: z.string().describe("Name of the WhatsApp chat to summarize"), }, async (args) => { const { chatName = "" } = args; // Find the chat by name // A real implementation would be more robust const targetChat = await chatService.findChatByName(chatName); // Get recent messages for analysis const messages = await messageService.getMessages(targetChat.id); const promptText = `Analyze this WhatsApp chat data for insights: Chat Information: - Chat Name: ${targetChat.name} - Chat Type: ${targetChat.isGroup ? "Group Chat" : "Individual Chat"} - Analysis Type: summary Analysis Focus: Provide a comprehensive overview including key topics, sentiment, and notable patterns. Recent Messages (${messages.length} messages): ${messages.map((msg) => msg._serializedContent).join("\n")} Please provide a detailed summary.`; return { description: `Summary of WhatsApp chat: ${targetChat.name}`, messages: [ { role: "user", content: { type: "text", text: promptText, }, }, ], }; }, ); ``` This defines a prompt called `whatsapp_chat_summarizer` that takes a `chatName` argument and generates a formatted prompt with the chat data. ### How prompts work in practice The LLM client presents a list of available prompts to the user, who can then select one to use. When the user selects a prompt with arguments, the client should display a modal or form allowing the user to fill in the required arguments. Once the user submits the form, the MCP client sends a `prompts/get` request to the MCP server with the selected prompt and its arguments. The MCP server adds the relevant context to the prompt (in this case, the WhatsApp chat data) and returns the formatted messages to the MCP client. The client can then send these messages to the LLM for processing. This is especially useful for repetitive tasks where a user needs to combine tool call results with a complex prompt. If you can anticipate the user's needs, you can define a prompt that combines the necessary context and tool calls into a single reusable template. # What are MCP resources? Source: https://speakeasy.com/mcp/core-concepts/resources MCP resources are read-only, addressable content entities exposed by the server. They allow MCP clients to retrieve structured, contextual data (such as logs, configuration data, or external documents) that can be passed to models for reasoning. Because resources are strictly observational and not actionable, they must be deterministic, idempotent, and free of side effects. Resources can expose: - Log files - JSON config data - Real-time market stats - File contents - Structured blobs (for example, PDFs or images) Resources are accessed via URI schemes like `note://`, `config://`, or `stock://`, and read using the `resources/read` method. ## Resource lifecycles and request and response formats Each resource lifecycle follows this pattern: 1. The server registers a static resource or URI template (for example, `stock://{symbol}/earnings`). 2. The client calls `resources/list` to discover available resources or templates. 3. The client sends a `resources/read` request with a specific resource URI. 4. The server loads the content for that resource. 5. The server returns the content as either `text` or `blob`. Here is an example of a resource read request: ```json { "method": "resources/read", "params": { "uri": "stock://AAPL/earnings" }, "id": 8 } ``` The server responds with a text resource: ```json { "jsonrpc": "2.0", "id": 8, "result": { "contents": [ { "uri": "stock://AAPL/earnings", "mimeType": "application/json", "text": "{ \"fiscalDateEnding\": \"2023-12-31\", \"reportedEPS\": \"3.17\" }" } ] } } ``` Resources can also return `blob` values for binary content like base64-encoded PDFs or images. ## Declaring a resource in Python Resources are regular functions marked with decorators that define their role: - `@list_resources()` exposes static resources. You return a list of `types.Resource` items for static URIs. - `@list_resource_templates()` exposes dynamic resources. You return a list of `types.ResourceTemplate` items for dynamic, parameterized URIs. - `@read_resource()` implements logic for resolving a resource by URI. Here's an example using the Alpha Vantage API to return earnings data. Since we want to support arbitrary stock symbols, we'll use a resource template to let clients specify the symbol. ```python import asyncio from mcp.server import Server from mcp import types from mcp.server import stdio from mcp.server import InitializationOptions, NotificationOptions from pydantic import AnyUrl import requests app = Server("stock-earnings-server") @app.list_resource_templates() async def list_earnings_resources_handler(symbol: str) -> list[types.ResourceTemplate]: return [ types.ResourceTemplate( uriTemplate="stock://{symbol}/earnings", name="Stock Earnings", description="Quarterly and annual earnings for a given stock symbol", mimeType="application/json" ) ] @app.read_resource() async def read_earnings_resource_handler(uri: AnyUrl) -> str: parsed = str(uri) if not parsed.startswith("stock://") or not parsed.endswith("/earnings"): raise ValueError("Unsupported resource URI") symbol = parsed.split("://")[1].split("/")[0].upper() url = ( "https://www.alphavantage.co/query" "?function=EARNINGS" f"&symbol={symbol}" "&apikey=demo" ) response = requests.get(url) return response.text ``` In the example above, we: - **Declare a resource template `stock://{symbol}/earnings`** that clients can use to request dynamic resources for any stock symbol. - **Use `@list_resource_templates` to expose the template** to clients, which tells the agent how to construct valid URIs. - **Use `@read_resource` to handle execution**, where we fetch real-time earnings from the Alpha Vantage API for the provided stock symbol. - **Return a valid JSON text response**, which is wrapped by MCP in a `TextContent` resource and can be used as context for prompts or decisions. If the resource was static – for example, if the client only tracks Apple stock – we'd use the `@list_resources` decorator and return a list of `types.Resource` items instead: ```python ... @app.list_resources() async def list_apple_earnings_resource() -> list[types.Resource]: return [ types.Resource( uri="stock://AAPL/earnings", name="Apple Earnings", description="Quarterly and annual earnings for Apple Inc.", mimeType="application/json" ) ] ``` ## Best practices and pitfalls to avoid Here are some best practices for implementing MCP resources: - Use clear, descriptive URI schemes like `note://`, `stock://`, or `config://`. - Keep resource data consistent and read-only. - Validate inputs or file paths to prevent injections or errors. - Set correct MIME types so clients can parse content properly. - Support dynamic resources with URI templates. Here are some pitfalls to avoid: - Treating resources as action triggers (use [tools](/mcp/tools) instead). - Returning extremely large payloads (use pagination instead). - Exposing sensitive data (unless scoped by [roots](/mcp/roots) or authenticated context). - Relying on global state (unless explicitly isolated per session). MCP resources are intended for structured, factual, and often cached information. They are perfect for background context, factual grounding, or real-world reference material. ## Resource annotations Resources can include optional annotations that provide additional metadata to guide clients on how to use them: ```python from mcp.server import Server from mcp import types app = Server("annotated-resources-server") @app.list_resources() async def list_resources() -> list[types.Resource]: return [ types.Resource( uri="docs://company/earnings-2024.pdf", name="2024 Earnings Report", mimeType="application/pdf", annotations={ # Specify intended audience (user, assistant, or both) "audience": ["user", "assistant"], # Importance ranking from 0 (least) to 1 (most) "priority": 0.8 } ) ] ``` Annotations help clients decide: - **Which audience** should see the resource (the `user`, the `assistant`, or both). - **How important** the resource is (on a scale of `0.0` to `1.0`). Clients can use annotations to sort, filter, or highlight resources appropriately, but annotations are hints, not guarantees of behavior. ## Pagination support When dealing with large sets of resources, MCP supports pagination using cursors: ```python from mcp.server import Server from mcp import types app = Server("paginated-resources-server") @app.list_resources() async def list_resources(cursor: str = None) -> tuple[list[types.Resource], str]: # Fetch resources from your backend, database, etc. all_resources = fetch_all_resources() # Implementation of pagination page_size = 10 start_index = 0 if cursor: # Parse cursor to get starting position start_index = int(cursor) # Get the current page of resources current_page = all_resources[start_index:start_index + page_size] # Calculate next cursor if there are more items next_cursor = None if start_index + page_size < len(all_resources): next_cursor = str(start_index + page_size) return current_page, next_cursor ``` When implementing pagination: - Remember the `cursor` parameter can be any string, but typically encodes a position. - Return a tuple with both resources and the next cursor. - Return `None` for the cursor when there are no more pages. - Keep cursor values opaque to clients. They shouldn't assume structure. - Handle invalid cursors gracefully. Pagination helps manage memory usage for large resource collections and improves client responsiveness. ## Resources vs tools MCP resources are **read-only** and addressable via URIs like `note://xyz` or `stock://AAPL/earnings`. They are designed to preload context into the agent's working memory or support summarization and analysis workflows. MCP tools are **actionable** and invoked by the client with parameters to perform an action like writing a file, placing an order, or creating a task. To avoid decision paralysis, define resources according to **what the client should know** and tools according to **what the client can do**. ## Practical implementation example In a WhatsApp MCP server, you could expose all image attachments that the server downloads as resources. This way, the MCP client can access the images without having to call a tool every time. The MCP client can simply request the resource by its URL, and the MCP server will return the image data. Here's how to implement a resource using the TypeScript SDK: ```typescript import { ResourceTemplate } from "@modelcontextprotocol/sdk/server/mcp.js"; import { MediaRegistry } from "whatsapp"; mcpServer.resource( "media", new ResourceTemplate("media://{messageId}", { list: () => { const allMedia = mediaRegistry.getAllMediaItems(); const resources = []; for (const { mediaItems } of allMedia) { for (const mediaItem of mediaItems) { resources.push({ uri: `media://${mediaItem.messageId}`, name: mediaItem.name, description: mediaItem.description, mimeType: mediaItem.mimetype !== "unknown" ? mediaItem.mimetype : undefined, }); } } return { resources }; }, }), async (uri: URL, { messageId }) => { const messageIdRaw = Array.isArray(messageId) ? messageId[0] : messageId; const messageIdString = decodeURIComponent(messageIdRaw); const mediaData = await messageService.downloadMessageMedia(messageIdString); return { contents: [ { uri: uri.href, blob: mediaData.data, mimeType: mediaData.mimetype, }, ], }; }, ); ``` This code defines a resource called `media` that can be accessed by the MCP client. The resource has a `list` method that returns a list of all media items. The MCP client can then request a specific media item by its URI, and the MCP server will return the media data. ### Notifying clients about resource changes The MCP server can send a `notifications/resources/list_changed` message to notify the MCP client that the list of resources has changed: ```typescript import { MediaItem, MediaRegistry } from "whatsapp"; // When the MCP server detects that a new media item has been added mediaRegistry.onMediaListChanged((mediaItems: MediaItem[]) => { mcpServer.sendResourceListChanged(); }); ``` You can also send a notification when a specific resource has changed: ```typescript mediaRegistry.onMediaItemChanged((mediaItem: MediaItem) => { void mcpServer.server.sendResourceUpdated({ uri: resourceUri, }); }); ``` How the LLM client handles resources is up to the client implementation. The MCP server just needs to expose the resources and notify the client when they change. # What are MCP roots? Source: https://speakeasy.com/mcp/core-concepts/roots MCP roots are context-defining [URIs](https://en.wikipedia.org/wiki/Uniform_Resource_Identifier) that establish operational boundaries for MCP servers. They function as "safe zones" or "allowed directories" that an AI agent can access when interacting with your system. When an MCP client provides roots for servers, it's essentially saying, "You're allowed to work within these specific areas." While roots are sometimes described as a **security** feature, the MCP Specification does not dictate how servers must implement root handling. So when the client tells the server which areas it can work within, the server may ignore this rule and still be within the specification. Any suggestion that roots provide security needs to assume that the server will respect the boundaries set by the client. ## Why are MCP roots useful? MCP roots allow the user (or an LLM) to control **where** a server should focus or within which boundaries a server should operate. For example, if a user has a stock-trading MCP server, they may want their client to limit the trading server to only certain stocks or exchanges. In this example, the client will prompt the user to select a list of stocks and exchanges, and then notify the server that the list of roots has changed. The server will then request the new roots, and should limit its actions to the selected list of stocks and exchanges. ![Roots](/assets/mcp/building-servers/roots-diagram.png) Roots help in several important ways. - **Security**: If a server is known to respect root boundaries, roots act like fences that keep the server from accessing things it shouldn't. - **Focus**: Roots guide servers to look only at relevant information, like giving them a map of where to search. - **Performance**: By limiting where a server can look, the server may run faster and with less searching or filtering logic. - **Trust**: Users feel more comfortable knowing servers can only access specific areas they've approved. This still depends on the server's implementation of roots. MCP roots can point to different kinds of places: - **Folders on your computer**: `file:///home/user/projects/myapp` - **Websites and APIs**: `https://api.example.com/v1` - **Database connections**: `db://mycompany/customers` - **Special locations**: `note://meeting-notes` or `config://app-settings` ## How MCP roots work Here's how MCP roots could work in practice: 1. **Client to the server: _"Hello, I can use roots!"_** - The client tells the server it supports roots when they first connect. 2. **Server to the client: _"What roots do you have?"_** - The server asks the client for available roots using `roots/list`. 3. **Client to the server: _"Stay within these boundaries."_** - The client returns a list of roots. 4. **User to the client: _"I want to change the boundaries."_** - The user updates the boundaries the client and server should focus on. 5. **Client to the server: _"Roots have changed."_** - If needed, the client can tell the server when roots are added or removed using `notifications/roots/list_changed`. 6. **Server to the client: _"What roots do you have?"_** - The server requests a list of roots using `roots/list`. 7. **Client to the server: _"Stay within these boundaries."_** - The client responds with a list of roots. It bears repeating that while the client owns and defines the list of allowed roots, the server **should** stay within those boundaries but is free to implement root handling how it wants. The MCP Specification does not enforce that servers respect those boundaries. It only standardizes how roots are communicated. Here's an example of a roots list response from a client: ```json { "roots": [ { "uri": "file:///home/user/projects/myapp", "name": "My Application Code" }, { "uri": "https://api.example.com/v1", "name": "Example API Endpoint" } ] } ``` The `uri` indicates the precise resource location, while the optional `name` field gives the root an identifier that can be used by an LLM. ## Implementing MCP roots in Python Let's look at examples of how to implement MCP roots using [FastMCP](https://github.com/jlowin/fastmcp/): ### Server implementation ```python filename="server-example.py" from fastmcp import FastMCP import asyncio from mcp.server.stdio import stdio_server from mcp.server import InitializationOptions, NotificationOptions # Create an MCP server app = FastMCP("Roots Example Server") # Define roots using the standard resource pattern @app.resource("roots://list") def list_roots(): return { "roots": [ { "uri": "file:///home/projects/roots-example/frontend", "name": "Frontend Repository" }, { "uri": "https://api.openf1.org/", "name": "F1 API Endpoint" } ] } # Define a simple tool that processes a file @app.tool() def process_file(filename: str) -> str: return f"Processing file: {filename}" # Server entrypoint async def main(): async with stdio_server() as (read_stream, write_stream): await mcp.run( read_stream, write_stream, InitializationOptions( server_name="roots-example-server", server_version="0.1.0", capabilities=mcp.get_capabilities( notification_options=NotificationOptions(), experimental_capabilities={}, ), ), ) if __name__ == "__main__": asyncio.run(main()) ``` ### Client implementation ```python filename="client-example.py" from fastmcp import Client from fastmcp.client.transports import FastMCPTransport import asyncio async def client_example(): # Connect to a running MCP server and specify the roots we want to use async with Client(transport_url="stdio:", roots=["roots://list"]) as client: # Call a tool on the server result = await client.call_tool("process_file", {"filename": "data.csv"}) print(result) if __name__ == "__main__": asyncio.run(client_example()) ``` In these examples: - The server is initialized with a descriptive name. - Roots are defined using the `@app.resource` decorator with `roots://list`. - A simple tool demonstrates how tools can be used within the scope of roots. - The client specifies which roots it wants to use when connecting to the server. - The client calls a tool on the server, which will operate within the boundaries defined by roots. ## Implementation patterns Let's look at some examples of how we can use MCP roots within a server. ### File system access When granting access to file system directories, always verify that accessed paths are within approved roots: ```python def read_file(file_path): # Convert to absolute path abs_path = os.path.abspath(file_path) # Check if path is within any approved root for root in app.roots: if root.uri.startswith("file://"): root_path = root.uri.replace("file://", "") if abs_path.startswith(root_path): # Safe to read with open(abs_path, 'r') as f: return f.read() # Not within any approved root raise SecurityError(f"Access denied: {file_path} is outside approved roots") ``` ### API endpoint access Similarly, when making API calls, make sure they respect root boundaries: ```python async def make_api_call(endpoint, method="GET", data=None): # Check if endpoint is within an approved root for root in app.roots: if root.uri.startswith("https://") and endpoint.startswith(root.uri): # Safe to proceed async with httpx.AsyncClient() as client: response = await client.request(method, endpoint, json=data) return response.json() # Not within any approved root raise SecurityError(f"Access denied: {endpoint} is outside approved API roots") ``` ### Tool integration MCP roots also work with MCP tools. If a tool is granted access to a root, it can only operate on files within that root: ```python @app.tool("create_file") async def create_file(path: str, content: str): # Convert to absolute path abs_path = os.path.abspath(path) # Check against allowed file system roots for root in app.current_roots: if root.uri.startswith("file://"): root_path = root.uri.replace("file://", "") if abs_path.startswith(root_path): # Write the file with open(abs_path, 'w') as f: f.write(content) return {"success": True, "path": abs_path} return { "success": False, "error": "Permission denied: path is outside allowed roots" } ``` ### Resource integration Resources can also be filtered based on allowed roots: ```python @app.on_resources_list() async def list_resources(): all_resources = await get_all_available_resources() # Filter to only include resources within allowed roots allowed_resources = [] for resource in all_resources: for root in app.current_roots: if resource.uri.startswith(root.uri): allowed_resources.append(resource) break return allowed_resources ``` ### Roots vs tools MCP roots are distinct from [MCP tools](/mcp/tools), which are callable server-side actions. While MCP roots define where operations can occur, MCP tools define what actions can be performed. # What is MCP sampling? Source: https://speakeasy.com/mcp/core-concepts/sampling MCP sampling lets servers ask for LLM completions through the client. This means your server can send a request to the LLM and receive a completion to continue solving a task. MCP sampling is unique in that it flips the typical flow: Rather than clients always initiating requests, servers can ask for AI help when needed. This enables an MCP server to: - Use AI to make smart decisions based on available information. - Create structured data outputs in specific formats. - Complete multi-step workflows that require thinking. - Analyze and respond to external data. Sampling also allows for a human to be involved in the process: The user can review and approve both the request before sending it to the LLM and the completion before it's returned to the server. ## How sampling works Here's how MCP sampling works: 1. Your server sends a `sampling/createMessage` to the client while fulfilling a task. 2. **Human checkpoint one:** The client shows the user the exact prompt and context that will be sent to the LLM and the user may edit, approve, or reject this request. 3. If the user approves the request, the client requests a completion from the LLM. 4. **Human checkpoint two:** The client shows the user the full LLM response, which the user may edit, approve, or reject. 5. If the user approves the response, the client sends the approved response to the MCP server. This human-in-the-loop design ensures users maintain control over what the LLM sees and generates and also allows the server to use AI to enhance the task it's trying to complete. ![Sampling flow](/assets/mcp/building-servers/sampling-diagram.png) The beauty of this flow is that the human user maintains oversight. They can approve or reject the request before it's returned to the server. ### Request format Sampling requests use a standardized message format: ```typescript { messages: [ { role: "user" | "assistant", content: { type: "text" | "image", text?: string, data?: string, // base64 encoded mimeType?: string } } ], modelPreferences?: { hints?: [{ name?: string // Suggested model name/family }], costPriority?: number, // 0-1, importance of minimizing cost speedPriority?: number, // 0-1, importance of low latency intelligencePriority?: number // 0-1, importance of capabilities }, systemPrompt?: string, includeContext?: "none" | "thisServer" | "allServers", temperature?: number, maxTokens: number, stopSequences?: string[], metadata?: Record } ``` Here's what each field does: - `messages`: Conversation messages with `role` and `content` (which can be text or an image) - `modelPreferences`: Hints and priorities for model selection, for example, cost, speed, or intelligence - `systemPrompt`: An optional directive for model behavior - `includeContext`: Additional MCP context to include - **Sampling parameters:** Controls like temperature and token limits ### Response format The client returns a completion result from the LLM: ```typescript { model: string, // Name of the model used stopReason?: "endTurn" | "stopSequence" | "maxTokens" | string, role: "user" | "assistant", content: { type: "text" | "image", text?: string, data?: string, mimeType?: string } } ``` The response includes: - `model`: The LLM that was used (for example, `"claude-3.7-sonnet"` or `"gpt-4o-mini"`) - `stopReason`: Why the generation stopped - `role`: Typically `"assistant"` for model output - `content`: The actual response content (which can be text or an image) ## Example: Code review with MCP Here's an example of using sampling with an MCP-enabled dev tool integrated with an IDE. We want the language model to help us analyze and improve a code snippet. The server sends a sampling request containing the code and a prompt asking for refactoring suggestions. ### How it works 1. **Server request:** The MCP server sends a sampling request with a prompt asking for a code review. 2. **LLM sampling:** The client forwards the prompt to the LLM, which returns detailed suggestions as completions. 3. **Returned completion usage:** The returned completion can be used by the MCP server to apply approved suggestions automatically. Here's the sampling request from the server:
```json filename="samplingRequest.json" { "method": "sampling/createMessage", "params": { "messages": [ { "role": "user", "content": { "type": "text", "text": "codeToReview" // The code to review } } ], "systemPrompt": "You are a senior code reviewer with expertise in JavaScript. Provide detailed suggestions for refactoring the code.", "includeContext": "thisServer", "maxTokens": 150, "temperature": 0.4, "stopSequences": ["\n"] } } ```
```js filename="codeToReview.js" // Simple function with a common pattern to improve function greet(name) { if (name) { return "Hello, " + name + "!"; } else { return "Hello, guest!"; } } ```
Here's the returned completion:
```json filename="completion.json" { "model": "claude-3.7-sonnet", "stopReason": "endTurn", "role": "assistant", "content": { "type": "text", "text": "suggestedCode" // The LLM's suggested code } } ```
```js filename="suggestedCode.js" // Improved version with template literals and default parameter function greet(name = 'guest') { return `Hello, ${name}!`; } ```
## What else can you do with sampling? Sampling enables some powerful agentic patterns: **Decision making:** Sampling can generate structured outputs for conditional logic in your applications. An agent could analyze available data to recommend the next steps in a complex workflow or evaluate user input to determine which process to trigger. **Multi-step tasks:** With sampling, agents can chain multiple calls for complex workflows. This involves breaking down a complex problem into sequential steps and solving each, using the results of one step as input for the next while keeping the user in the loop. # What are MCP tools? Source: https://speakeasy.com/mcp/core-concepts/tools import { Callout } from "@/mdx/components"; MCP tools are callable functions that MCP servers expose to the client. They allow clients to interact with real-world external environments, for example, by querying databases, calling APIs, writing files, triggering workflows, and more. Tools are how clients can perform actions and trigger side effects using MCP. ## Lifecycle and request and response formats Each tool lifecycle follows this pattern: 1. The server registers a tool with a name, input parameters, output schema, and description. 2. The client calls `tools/list` to discover available tools. 3. The client calls `tools/call` with the tool `name` and `arguments`. 4. The server calls the tool function with the provided arguments. 5. The server runs the tool and returns a result or an error message. Clients call tools using the `tools/call` method with structured arguments. Here is an example of a tool call request payload: ```json { "method": "tools/call", "params": { "name": "write_note", "arguments": { "slug": "morning", "content": "Start your day with clarity and confidence." } }, "id": 2 } ``` The `name` field is the name of the tool to call. The `arguments` field is an object containing the arguments to pass to the tool. The `id` field is a unique identifier for the request. The response to a tool call has the following structure: ```json { "jsonrpc": "2.0", "id": 2, "result": { "status": "saved", "slug": "morning" } } ``` If an error occurs, the response must conform to the standard JSON-RPC error format. ```json { "jsonrpc": "2.0", "id": 2, "error": { "code": 2050, "message": "Invalid parameters", "data": { "name": "write_note", "arguments": { "slug": "morning", "content": "Start your day with clarity and confidence." } } } } ``` ## Declaring a tool request in Python To declare a tool, you annotate a function with the `@tool` decorator. A tool must accept and return only JSON-serializable data types such as `str`, `int`, `float`, `bool`, `list`, `dict`, and `None`. The example below shows how to declare a tool using the [official Python SDK for MCP servers](https://github.com/modelcontextprotocol/python-sdk) to place a stock trade using the Alpaca API. ```python from mcp.server import Server from mcp import types from mcp.server import stdio import requests import asyncio app = Server("alpaca-order-server") ALPACA_API_KEY = "your_api_key" ALPACA_API_SECRET = "your_api_secret" ALPACA_BASE_URL = "https://paper-api.alpaca.markets/v2" HEADERS = { "APCA-API-KEY-ID": ALPACA_API_KEY, "APCA-API-SECRET-KEY": ALPACA_API_SECRET, "Content-Type": "application/json" } @app.list_tools() async def list_tools() -> list[types.Tool]: return [ types.Tool( name="place_stock_order", description="Place a stock trade via the Alpaca paper trading API", inputSchema={ "type": "object", "properties": { "symbol": {"type": "string"}, "qty": {"type": "integer"}, "side": {"type": "string", "enum": ["buy", "sell"]}, "order_type": {"type": "string", "enum": ["market", "limit"]}, "time_in_force": {"type": "string", "enum": ["day", "gtc"]}, "limit_price": {"type": "number"} }, "required": ["symbol", "qty", "side", "order_type", "time_in_force"] } ) ] @app.call_tool() async def call_tool(name: str, arguments: dict) -> list[types.TextContent]: if name != "place_stock_order": raise ValueError(f"Tool not found: {name}") symbol = arguments["symbol"].upper() qty = arguments["qty"] side = arguments["side"].lower() order_type = arguments["order_type"].lower() time_in_force = arguments["time_in_force"].lower() limit_price = arguments.get("limit_price") order_payload = { "symbol": symbol, "qty": qty, "side": side, "type": order_type, "time_in_force": time_in_force } if order_type == "limit" and limit_price: order_payload["limit_price"] = limit_price try: response = requests.post( f"{ALPACA_BASE_URL}/orders", headers=HEADERS, json=order_payload ) data = response.json() message = f"Order placed: {side.upper()} {qty} {symbol} @ {order_type.upper()}" return [types.TextContent(type="text", text=message)] except Exception as e: error_msg = f"Failed to place order for {symbol}: {str(e)}" return [types.TextContent(type="text", text=error_msg)] ``` In this example, we: - **Register a tool called `place_stock_order`** with a clear description and a JSON Schema specifying required inputs like `symbol`, `qty`, `side`, `order_type`, and `time_in_force`. - **Use `@list_tools` to expose the tool metadata** to the client. This is how agents and UI tools like Claude Desktop discover available tools dynamically at runtime. - **Implement `@call_tool` to handle the execution logic.** When the tool is invoked by the client, the server builds an HTTP request to the Alpaca API using the provided arguments. - **Format a confirmation message summarizing the placed order.** The result is returned as a `TextContent` object, which is compatible with standard MCP tool responses. We also make sure errors and missing data are handled gracefully, returning fallback messages if the API request fails. The Python SDK also provides a higher-level interface called `FastMCP`, originally introduced in [FastMCP 1.0](https://github.com/jlowin/fastmcp/releases/tag/v1.0), which simplifies tool declarations. The example above uses the low-level API, which is recommended only when you need fine-grained control over the tool lifecycle. ## Error handling MCP distinguishes between two types of errors, which must be handled in different ways. ### 1. Protocol-level errors Use protocol-level errors (such as thrown exceptions or JSON-RPC error responses) only in the following cases: - Tool not found - Permission denied - Invalid parameter format - Server-side exceptions unrelated to tool functionality ```python @app.call_tool() async def call_tool(name: str, arguments: dict) -> list[types.TextContent]: # Protocol-level error - tool not found if name not in AVAILABLE_TOOLS: raise ValueError(f"Tool not found: {name}") ``` ### 2. Tool execution errors Use structured error responses within successful JSON-RPC responses for: - Logic errors during tool execution - Invalid values - Failed operations - Any error the LLM should see and handle ```python @app.call_tool() async def call_tool(name: str, arguments: dict) -> list[types.TextContent]: if name == "place_order": try: # Attempt the operation result = place_order(arguments) return [types.TextContent(type="text", text=result)] except OrderError as e: # Tool execution error - returned as content with isError=True return [types.TextContent( type="text", text=f"Error placing order: {str(e)}" )] ``` This distinction matters because, while protocol-level errors aren't seen by the LLM, tool execution errors are returned as content, allowing the model to reason about errors and retry or change its strategy. ## Best practices and pitfalls to avoid Here are some best practices to follow when writing MCP tools: - **Keep names clear and purposeful:** Agents or LLMs can use tool names in decision-making. - **Validate all inputs:** Never assume the client sends valid or safe data. - **Write good docstrings:** These become the tool descriptions in discovery and are often seen by agents or LLMs. - **Return structured results:** Use JSON objects with consistent shape and semantics. - **Keep tools deterministic:** Clients expect the same inputs to produce the same outputs unless side effects are explicit and intentional. Avoid these common pitfalls: - Irreversible side effects without confirmation. Use [sampling](/mcp/sampling) for validation if needed. - Unsafe commands (for example, `os.system`) or raw DB access. - Complex or deeply nested input schemas that are hard to validate. - Global state unless scoped per session. - Returning raw strings unless free-form output is the intent. MCP tools are often confused with [MCP resources](/mcp/resources), which are named, read-only data references addressable via URIs. Learn more about the difference in the Resources vs Tools section of the [MCP resources documentation](/mcp/resources#resources-vs-tools). # What are MCP transports? Source: https://speakeasy.com/mcp/core-concepts/transports So, you've built your first MCP server – you've defined some tools, exposed a few resources, and maybe even wired up prompts – but how do you actually connect to it? That's where transports come in. MCP transports define how messages move between clients and servers. While MCP is a model-agnostic, JSON-RPC-based protocol, the transport layer handles the real-world details of sending and receiving data – whether you're embedding the server in a local CLI, streaming updates to a browser, or wiring it into a desktop agent. MCP currently supports two standard forms of transport: `stdio` and `SSE`. Custom transports that conform to the standard input and output (stdio) and message-handling semantics are also supported. ## Request, response, and notification formats All communication follows the JSON-RPC 2.0 wire protocol. Every message sent between the client and the server falls into one of these categories: ### Request The client sends the following payload to invoke a method on the server: ```json { "jsonrpc": "2.0", "id": 1, "method": "tools/call", "params": { "name": "place_order", "arguments": { ... } } } ``` ### Response The server returns the following data after handling a request: ```json { "jsonrpc": "2.0", "id": 1, "result": { "status": "ok" } } ``` ### Notification A notification is a one-way message that does not expect a response. ```json { "jsonrpc": "2.0", "method": "notifications/prompts/list_changed", "params": { "updated": true } } ``` ## Built-in MCP transports MCP servers can be exposed over two built-in transport mechanisms: ### stdio Standard input and output (stdio) is the simplest and most universal transport for MCP. It allows a server to read from `stdin` and write to `stdout`, which is ideal for local use, CLIs, and agent plugins. Other use cases are: - CLI tools - Local development - Agent plugins (like Claude Desktop or LangGraph steps) Here is a Python implementation of an MCP transport with `stdio`: ```python from mcp.server import Server from mcp.server import stdio_server app = Server("stdio-mcp-server") @app.list_tools() async def tools(): return [] async def main(): async with stdio_server() as (read_stream, write_stream): await app.run( read_stream, write_stream, app.create_initialization_options() ) if __name__ == "__main__": import asyncio asyncio.run(main()) ``` In the example above, we: - **Register an empty list of tools** via `@list_tools`, which could later be populated with callable server-side functions. - **Use `stdio_server()` as the transport**, which opens standard input and output streams for communication and is ideal for local processes and CLI integrations. - **Run the server** with `app.run(...)`, passing the opened streams and initialization options to handle incoming JSON-RPC messages and serve responses. ### SSE Server-sent events (SSE) provide a persistent connection for server-to-client streaming, while client-to-server messages are sent using HTTP `POST`. It's useful for interactive UIs, dashboards, or environments where long-lived connections are preferred. Here is an implementation of an MCP server serving via SSE: ```python from mcp.server import Server from mcp.server.sse import SseServerTransport from starlette.applications import Starlette from starlette.routing import Route app = Server("sse-mcp-server") sse = SseServerTransport("/messages") async def sse_handler(scope, receive, send): async with sse.connect_sse(scope, receive, send) as streams: await app.run(streams[0], streams[1], app.create_initialization_options()) async def post_handler(scope, receive, send): await sse.handle_post_message(scope, receive, send) starlette_app = Starlette(routes=[ Route("/sse", sse_handler), Route("/messages", post_handler, methods=["POST"]) ]) ``` In the example above, we: - **Initialize the SSE transport** using `SseServerTransport("/messages")`, which defines the internal `POST` message endpoint that the server will listen to for client inputs. - **Define a `/sse` endpoint** via `sse_handler`, which establishes a persistent SSE connection. Inside it, we call `app.run()` with the input and output streams from the connected client. - **Define a `/messages` `POST` route** with `post_handler`, allowing the client to send messages back to the server via HTTP `POST`, which is how the client communicates with the server. - **Wrap the handlers in a Starlette app** using route definitions, making it ready to be served by any ASGI server (for example, Uvicorn). ## Custom transport implementations MCP does not require servers to use `stdio` or `SSE`. Any transport can be used, as long as it: 1. Supports bidirectional messaging via JSON-RPC 2.0. 2. Can be hooked into the server's `run(read_stream, write_stream, initialization_options)` method. 3. Implements proper connection lifecycle and error handling. So, you can use: - WebSockets - gRPC - Shared memory channels - Named pipes - Custom browser bridges You must ensure that the messages follow the JSON-RPC structure and that your transport yields the appropriate async streams. ## Connection lifecycle and control Regardless of the transport used, all MCP connections follow the same lifecycle: ### Initialization ![Initialization](/assets/mcp/building-servers/transport-init.png) 1. The client connects and sends an `initialize` request with its capabilities. 2. The server responds with its capabilities and protocol version. 3. The client acknowledges with `notifications/initialized`. ### Active session During an active session, both clients and servers can: - Make requests that expect responses. - Send notifications that don't expect responses. - Cancel in-flight requests with `notifications/cancelled`. - Send progress updates with `notifications/progress` (although this is not yet implemented by the Python MCP SDK). ### Termination Either side can terminate the connection by: - Closing the transport. - Sending a protocol-specific termination message. - Disconnecting without notification (although clean termination is preferred). ## Final thoughts Transports should be secured according to their communication layers: - For `SSE`, **validate Origin headers**, enforce **authentication**, and **bind to `localhost`** during local development to avoid DNS rebinding vulnerabilities. - For custom transports, ensure that **authentication**, **rate limiting**, and **input sanitization** are enforced. Always follow the [JSON-RPC security best practices](https://www.jsonrpc.org/specification) and sanitize all payloads. The transport may be low-level but it defines the perimeter of your server, so **treat it as a security boundary**. An MCP server can support multiple transport mechanisms simultaneously, but it must implement at least one transport to be operational. Each client connection uses one specific transport, but the server itself can expose its functionality through different transport interfaces based on various client needs. | Use case | Recommended transport | | ----------------------------------------- | --------------------- | | Local development | `stdio` | | CLI tools | `stdio` | | Agent plugins (LangGraph, Claude Desktop) | `stdio` | | Server UIs / browsers | `SSE` | | Cross-network communication | `SSE` with CORS/Auth | | Custom hardware or IPC | Custom | | WebSocket or real-time streaming | Custom (WebSocket) | # Deploying remote MCP servers Source: https://speakeasy.com/mcp/deploying-mcp-servers import { Callout, Table } from "@/mdx/components"; It is still early days for the Model Context Protocol (MCP), and even though it is already widely implemented, the MCP Specification is [changing rapidly](https://modelcontextprotocol.io/development/roadmap). For example, in the [March 2025 update](https://modelcontextprotocol.io/specification/2025-03-26/changelog), a new transport, ["streamable HTTP"](https://modelcontextprotocol.io/specification/2025-03-26/basic/transports#streamable-http) was added to the specification, along with an optional [authorization framework](https://modelcontextprotocol.io/specification/2025-03-26/basic/authorization), based on OAuth 2.1. Keeping up with these changes feels a bit like treading water, but the water is alphabet soup, and someone keeps dumping more letters in. In this guide, we'll focus on one specific topic: hosting remote MCP servers safely. As part of this, we'll cover the new streamable HTTP transport and the authorization framework. ## Remote MCP transports Most of the early MCP servers began as small programs running on a developer's laptop, connected to MCP clients through local `stdio`. Indeed, as of writing this, the very first example on the MCP website still shows all servers running on "your computer". The specification, however, always included server-sent events (SSE) as a transport mechanism, allowing MCP clients to connect to remote servers via HTTP, and allowing servers to stream responses back to clients. Besides the risk of prompt injection, the HTTP+SSE transport part of the specification seems the most contentious in the recent online discourse around MCP. This intensified after the release of the March 2025 update, with many commenters concluding that "streamable HTTP" is essentially HTTP+SSE with more steps and speculating that it will one day converge on something akin to WebSocket. We're not throwing a hat in the ring with this debate today, but we'll say this: The specification in its current state is useful without immediate changes to the transport mechanism. Relying on SSE and the normal HTTP pipeline avoids the complexities of upgrading connections from HTTP to WebSocket. With the addition of session IDs passed via headers in HTTP+SSE, maintaining stickiness in serverless or proxied servers is trivial. Waiting for all the details to be ironed out feels like debating tabs vs spaces at this point. With that out of the way, let's use the protocol we have, even if not everyone agrees about the current direction. ## Reasons to host MCP servers remotely Remote MCP servers allow teams to connect to a common server that is always available, which avoids server-version disparity among team members and prevents secrets (such as organization-level API keys) from being shared with users. If your users are less technical, you can also provide a simple web interface to the server, allowing them to run servers and use tools without needing to know how to use the command line or Docker. Let's look at the options available for hosting remote MCP servers. ## Choosing your hosting model
The protocol is identical across models: An HTTPS endpoint accepts JSON-RPC 2.0 over an HTTP `POST` (optionally upgrading to SSE) and streams the response. Only the infrastructure changes. ## Deploying Cloudflare Workers The Cloudflare blog post, [Build and deploy Remote Model Context Protocol (MCP) servers to Cloudflare](https://blog.cloudflare.com/remote-model-context-protocol-servers-mcp/), walks readers through all the tools and steps needed to deploy a remote MCP server to Cloudflare Workers. Let's summarize the steps here by modifying the [Sentry MCP](https://github.com/getsentry/sentry-mcp) server and deploying it to Cloudflare Workers. ### Requirements You'll need a [Cloudflare](https://www.cloudflare.com/) account (free is fine), and [Node.js](https://nodejs.org/en/download) with [pnpm](https://pnpm.io/installation) installed locally. Our example uses the Sentry MCP Server, so you'll also need a Sentry account. ### Starting with a template If you're building your own server, you can start with one of the many [Cloudflare demos](https://github.com/cloudflare/ai/tree/main/demos). But to get something useful up and running in this guide, we decided to use the [Sentry MCP Server](https://github.com/getsentry/sentry-mcp) (itself based on the [mcp-github-oauth](https://github.com/cloudflare/ai/tree/main/demos/remote-mcp-github-oauth) demo) as an example. Clone the Sentry MCP Server: ```bash git clone https://github.com/getsentry/sentry-mcp cd sentry-mcp ``` ### Installing dependencies Install the dependencies using pnpm: ```bash pnpm install ``` ### Authenticating Wrangler with Cloudflare Wrangler is Cloudflare's CLI for deploying and managing Workers. We'll use `npx` to run it without installing globally. Check that you can run `npx wrangler`: ```bash npx wrangler --version ``` This should print the version of Wrangler. If you get an error, you may need to install Node.js or add it to your PATH. Next, you'll need to authenticate Wrangler with your Cloudflare account. This is a one-time step: ```bash npx wrangler login ``` This opens a browser window and asks you to log in to your Cloudflare account. Once you've logged in, you'll be redirected to a page that says, **You have granted authorization to Wrangler!** You can close this page. ### Creating a Cloudflare KV namespace The Sentry MCP Server uses a Cloudflare KV namespace to store OAuth tokens. You can create a new KV namespace using the Wrangler CLI: ```bash npx wrangler kv namespace create "OAUTH_KV" ``` This creates a new KV namespace and prints the ID. Copy this ID, as you'll need it in the next step. ### Configuring the project Now that you have your KV namespace set up, you can configure the project. The `packages/mcp-cloudflare/wrangler.jsonc` file contains all the configuration for your Cloudflare Worker. Update the `kv_namespaces` section with the ID of the KV namespace you created earlier: ```jsonc filename="packages/mcp-cloudflare/wrangler.jsonc" focus=39 /** * For more details on how to configure Wrangler, refer to: * https://developers.cloudflare.com/workers/wrangler/configuration/ */ { "$schema": "node_modules/wrangler/config-schema.json", "name": "sentry-mcp", "main": "./src/server/index.ts", "compatibility_date": "2025-03-21", "compatibility_flags": [ "nodejs_compat", "nodejs_compat_populate_process_env", ], // we ask people to configure environment variables in prod "keep_vars": true, "migrations": [ { "new_sqlite_classes": ["SentryMCP"], "tag": "v1", }, ], "assets": { "directory": "public", "binding": "ASSETS", "not_found_handling": "single-page-application", }, "vars": {}, "durable_objects": { "bindings": [ { "class_name": "SentryMCP", "name": "MCP_OBJECT", }, ], }, "kv_namespaces": [ { "binding": "OAUTH_KV", "id": "8dd5e9bafe1945298e2d5ca3b408a553", }, ], "ai": { "binding": "AI", }, "observability": { "enabled": true, "head_sampling_rate": 1, }, "tail_consumers": [ // super noisy - disable until it can be improved // { "service": "sentry-mcp-tail" } ], "dev": { "port": 8788, }, } ``` ### Creating a Sentry API application Next, you'll need to create a Sentry API application. This is a one-time step that allows the MCP server to authenticate with Sentry. 1. Log in to your Sentry account. 2. Click on your profile picture in the top-left corner to open the menu. 3. Click on **User settings**. 4. Under **API**, click on **Applications**. 5. Click on **Create New Application**. Take note of the **Client ID** and **Client Secret**. You'll need these in the next step. ### Configuring the environment variables The Sentry MCP Server uses environment variables to configure the OAuth flow. You can set these using Cloudflare Wrangler: Change the working directory to `packages/mcp-cloudflare`, where the `wrangler.jsonc` file is located: ```bash cd packages/mcp-cloudflare ``` Now, set the environment variables in Cloudflare using the following commands: ```bash # Copy the values from your Sentry API application npx wrangler secret put SENTRY_CLIENT_ID # Paste the Client ID from Sentry npx wrangler secret put SENTRY_CLIENT_SECRET # Paste the Client Secret from Sentry # Generate a random string and copy it # (you can also use a password manager to generate this) openssl rand -base64 32 npx wrangler secret put COOKIE_SECRET # Paste the random string you generated ``` When prompted, enter the values you copied from the Sentry API application and the random string you generated. This stores the secrets in your Cloudflare account. ### Building the project Now that you have everything set up, you can build the project. Run the following command in the `packages/mcp-cloudflare` directory: ```bash pnpm build ``` ### Deploying the server Next, you can deploy the server to Cloudflare Workers. Run the following command in the `packages/mcp-cloudflare` directory: ```bash npx wrangler deploy ``` Cloudflare will build the project and deploy it to your account. This may take a few minutes. Once the deployment is complete, you should see a message like this: ```text Uploaded sentry-mcp (12.94 sec) Deployed sentry-mcp triggers (1.33 sec) https://sentry-mcp..workers.dev Current Version ID: ``` ### Testing the server The Sentry MCP repository includes a documentation page that explains how to use the server. Click the link Cloudflare Workers provides to open the docs page in your browser. ![Screenshot of the Sentry MCP web interface with a dark-themed background. The header displays the Sentry MCP logo, version 0.7.1, and a GitHub button in the top right. The main content introduces Sentry MCP as a Model Context Provider for interacting with the Sentry API. A highlighted quote from David Cramer reads: MCP is pretty sweet. Cloudflare's support of MCP is pretty sweet. Sentry is pretty sweet. So we made an MCP for Sentry on top of Cloudflare. Below, a section titled What is a Model Context Provider explains it as a way to plug the Sentry API into an LLM, enabling users to ask questions about their data in context. A simple diagram illustrates the flow between User, MCP, Sentry, and Cursor, with speech bubbles showing how the system helps fix issues. The overall tone is professional and informative, designed to welcome developers and provide clear guidance.](/assets/mcp/remote-servers/mcp-server-browser.png) Now let's test the MCP server. Open a terminal and run the following command: ```bash pnpx @modelcontextprotocol/inspector@0.11.0 ``` This starts the MCP Inspector, which allows you to interact with the MCP server in your browser. ![The MCP Inspector interface shows a browser-based tool for testing MCP connections. The dark-themed interface displays multiple panels: a left sidebar with connection options showing SSE transport selected and a server URL field, a main content area showing connection status as Pending, and a right panel with request/response details. The inspector appears ready to establish a connection to test a remote MCP server, with status indicators and connection controls prominently displayed.](/assets/mcp/remote-servers/mcp-inspector.png) ### Setting the redirect URL in Sentry Before you can connect the MCP Inspector to the Sentry MCP Server, you need to set the redirect URL in Sentry. This is the URL that Sentry redirects to after the OAuth flow is complete. 1. In a new browser tab, go back to the Sentry API application you created earlier. 2. Click on the application name to open the details. 3. Under **Redirect URIs**, add the OAuth callback URL: ```text https://sentry-mcp..workers.dev/oauth/callback ``` There is no save button. Unfocus the field, and Sentry should display a toast message indicating that the redirect URL was updated successfully. You can close this tab. ### Connecting the MCP Inspector to the Sentry MCP Server Now that you have the redirect URL set up, you can connect the MCP Inspector to the Sentry MCP Server. In the inspector window: 1. Select the SSE transport. 2. Enter the URL of your MCP server (for example, `https://sentry-mcp..workers.dev/sse`). 3. Click **Connect**. This establishes a connection to the MCP server, which kicks off the OAuth flow. You should see a message in the inspector window indicating that the MCP Inspector is requesting authorization: ![The Sentry MCP OAuth authorization dialog is open in a browser window. It displays the Sentry MCP logo and title at the top, followed by a box stating MCP Inspector is requesting access. Inside the box, details are shown: Name MCP Inspector, Website https://github.com/modelcontextprotocol/inspector, Redirect URIs http://127.0.0.1:6274/oauth/callback. Below, a message explains that the MCP Client is requesting authorization to Sentry MCP and that approval will redirect to complete authentication. Two buttons are at the bottom: Cancel and Approve, with Approve highlighted in light purple.](/assets/mcp/remote-servers/mcp-sentry-auth.png) If you look at the URL in the address bar, you'll notice that the redirect URL is set to your local MCP Inspector URL. This is expected, as the MCP Inspector is running locally and will handle the OAuth client flow. Also of note is that the OAuth page you're looking at is hosted on the Sentry MCP Server, not on Sentry itself. This is because the Sentry MCP Server is acting as a proxy for the OAuth flow. The MCP Inspector is requesting authorization to access your Sentry account, and the Sentry MCP Server is handling the redirect. The MCP Specification recommends this approach, as it allows the MCP server to act as a trusted intermediary between the client and the OAuth provider. There are, in effect, _two_ OAuth flows happening here: 1. The MCP Inspector is requesting authorization to access your Sentry MCP Server. 2. The Sentry MCP Server is requesting authorization to access your Sentry account on behalf of the MCP Inspector. Click **Approve**. The MCP Inspector will then redirect to the Sentry OAuth page for your app. ![A screenshot shows the Sentry OAuth authorization dialog for Useful Goshawk, a Sentry API application, requesting access to a Sentry account linked to ritza@example.com. The dialog lists permissions, including reading and writing access to organization details, teams, projects, and events. Two buttons labelled Approve and Deny are at the bottom. The interface has a clean, modern design with a purple sidebar and a light background featuring faint Sentry-themed icons.](/assets/mcp/remote-servers/mcp-sentry-approve.png) In the screenshot above, "Useful Goshawk" is the name of the Sentry API application we created earlier. The permissions listed are the scopes that the MCP Inspector is requesting access to. You can review these permissions and decide whether to approve or deny the request. Click **Approve** to grant the MCP Inspector access to your Sentry account. This will redirect you back to the MCP server, which will redirect you back to the MCP Inspector. ![A screenshot shows the MCP Inspector web interface focused on the Tools tab. The main panel displays a list of available tools, including list_organizations and list_teams, with descriptions explaining their functions. The right panel shows details for the list_organizations tool and a prominent Run Tool button. The interface uses a clean, modern design with a light background and clear section headers. Text in the image includes: Tools, List Tools, Clear, list_organizations, List all organizations that the user has access to in Sentry. Use this tool when you need to: View all organizations in Sentry, Find an organization's slug to aid other tool requests, list_teams, and Run Tool.](/assets/mcp/remote-servers/mcp-sentry-list-tools.png) Click on **Tools** in the top navigation bar, then on **List Tools**. This will show you a list of available tools on the MCP server. You can click on any tool to see its details and run it. We clicked on **list_organizations** to see a list of all organizations that the user has access to in Sentry. This is a good way to verify that the OAuth flow is working correctly and that the MCP server can access your Sentry account. Now click the **Run Tool** button to execute the tool. This will send a request to the MCP server, which will then forward it to Sentry and return the response. ![MCP Inspector web interface showing the Tool Result panel after running the list organizations tool. The right panel displays a white background with a green Success status and a code block containing: Organizations, the organization name, Web URL https://url.sentry.io, Region URL https://us.sentry.io, and guidance that the organization name is used as an identifier and regionUrl should be passed if supported. The interface is clean and modern with a light background, presenting information in a professional and informative manner to help users confirm MCP server access to Sentry organizations.](/assets/mcp/remote-servers/mcp-list-organizations.png) The response shows a list of organizations that you have access to in Sentry. This confirms that the MCP server can access your Sentry account and that the OAuth flow is working correctly. Now everyone on your team can use the Sentry MCP Server with their IDEs, LLMs, or any other MCP client. Of course, if you don't need to change the server code, you could just as well use the [hosted Sentry MCP Server](https://mcp.sentry.dev/). ## On-premises deployment with Cloudflare tunnels While Cloudflare Workers provides an easy way to deploy remote MCP servers, you may have reasons to run servers on your own infrastructure instead. When your organization has strict data handling requirements or MCP servers need access to services on your network, you can host your servers on-premises or in a private cloud. The [Polar.sh MCP server](https://docs.polar.sh/integrate/mcp) is a great example of a server that you might want to run on-premises. It allows AI assistants to interact with your Polar data, including subscriptions, posts, and other content. You may want to run this server on your own infrastructure for security, compliance, or performance reasons. In this example, we'll deploy the Polar MCP server on-premises and make it securely accessible from anywhere using a [Cloudflare Tunnel](https://developers.cloudflare.com/cloudflare-one/connections/connect-networks/) combined with [Zero Trust access](https://developers.cloudflare.com/cloudflare-one/policies/access/) controls. ### Reasons to use Cloudflare Tunnel with Zero Trust When hosting an MCP server on-premises, you have several options for making it remotely accessible: 1. Open a port on your firewall (not recommended for security reasons). 2. Set up a VPN (adds client complexity and management overhead). 3. Use a reverse proxy with authentication (requires careful configuration). 4. **Use Cloudflare Tunnel with Zero Trust** (our recommended approach). Cloudflare Tunnel creates an encrypted tunnel between your local server and Cloudflare's edge, with no inbound ports required. Combined with Cloudflare Zero Trust, you can restrict access to specific authenticated users without exposing your server to the public internet. ### Requirements You'll need: - A machine to host the Polar MCP server (Linux recommended) - [Docker](https://docs.docker.com/get-docker/) installed - A [Cloudflare](https://www.cloudflare.com/) account with: - A domain added to Cloudflare - Zero Trust enabled (free tier works for basic setups) ### Generating a Polar access token To allow the on-premises MCP server to authenticate with the Polar API, you need to generate an organization access token from the Polar dashboard. The steps below describe this process, referencing the user interface elements shown in the screenshot that follows. Log in to your Polar organization on [sandbox.polar.sh](https://sandbox.polar.sh) for testing. For production environments, you would typically use [polar.sh](https://polar.sh). 1. In the left-hand navigation sidebar, click **Settings**. 2. On the Settings page, locate the **Developers** section. Click **New Token** to open the **Create Organization Access Token** dialog. 3. In the **Name** field, enter a descriptive name for your token, such as `polar-mcp`. 4. Select an **Expiration** period for the token from the dropdown menu, for example, `7 days`. 5. Under **Scopes**, ensure all permissions are selected by checking each corresponding box. This grants the MCP server the necessary access to interact with your Polar data. 6. Scroll down and click **Create**. 7. Polar displays the generated access token only once upon creation. Copy this token and store it. You will need this value for the `POLAR_ACCESS_TOKEN` environment variable when you configure your Docker container in a later step. ![A screenshot of the Polar web interface shows the Create Organization Access Token dialog, which contains fields for Name, Expiration, and Scopes. The Name field is filled with polar-mcp. Expiration is set to 7 days. The Scopes section lists multiple permissions, each with a checkbox, including openid, profile, email, user:read, organizations:read, organizations:write, custom_fields:read, custom_fields:write, discounts:read, discounts:write, checkout_links:read, checkout_links:write, checkouts:read, checkouts:write, and products:read. All checkboxes are selected. The wider interface shows a sidebar with navigation options such as Home, Products, Benefits, Customers, Sales, Analytics, Finance, and Settings, with Settings highlighted.](/assets/mcp/remote-servers/polar-token.png) ### Setting up the Polar MCP server First, let's get the Polar MCP server running locally. Create a new directory for your Polar MCP deployment: ```bash mkdir polar-mcp cd polar-mcp ``` Create a `docker-compose.yml` file with the following content: ```yaml filename="docker-compose.yml" services: polar-mcp: image: node:24 container_name: polar-mcp restart: unless-stopped command: > npx -y --package @polar-sh/sdk -- mcp start --access-token your_polar_api_key --port 3000 --transport sse ports: - "127.0.0.1:3000:3000" # Only bind to localhost ``` Replace `your_polar_api_key` with the actual Polar API key you generated in the previous step. Start the Polar MCP server: ```bash docker compose up -d ``` Verify that the server is running correctly: ```bash curl http://localhost:3000/sse ``` If everything is working, you should see a response with a session ID and `/message` endpoint. ```text event: endpoint data: /message?sessionId=6f884816-1c0b-4543-8465-025cd55c7227 ``` ### Setting up Cloudflare Zero Trust Now let's configure Cloudflare Zero Trust to secure access to your MCP server. 1. Log in to your Cloudflare account. 2. Navigate to the **Zero Trust** dashboard. 3. If this is your first time using Cloudflare Zero Trust, you'll need to create a Zero Trust organization. 4. In the Zero Trust dashboard, go to **Access** > **Applications**. 5. Click **Add an application**. 6. Select **Self-hosted** as the application type. 7. Configure the application. - **Application name**: Enter the name `Polar MCP`. - **Session duration**: Choose how long users stay authenticated (for example, 24 hours). - **Application domain**: Choose **+ Add public hostname** and add a subdomain where your MCP server will be available (for example, `polar-mcp.yourdomain.com`). - Leave other settings at default values for now. 8. Create an access policy: - Click **Add a policy**. - **Policy name**: Enter the name `Polar MCP Access`. - **Action**: Allow. - **Configure rules**: Select who should have access to your MCP server. - For testing, you can use **Emails** and add your email address. - For a team, you might use **Emails ending in** your company domain. - Click **Save** 9. Click **Next** on the application. You can leave the default selected values for the optional settings. 10. Finish by clicking **Save** to create the application. 11. Make sure that the policy you created is linked to the application's policies on the application's **Policies** tab. ### Installing and configuring Cloudflare Tunnel Next, we'll set up a tunnel to securely connect your generated server to Cloudflare. 1. In the Cloudflare Zero Trust dashboard, navigate to **Networks** > **Tunnels**. 2. Click **Create a tunnel**. 3. Select **Cloudflared** as the tunnel type. 4. Give your tunnel a name (for example, `polar-mcp-tunnel`). 5. Click **Save tunnel**. Select **Docker** as the installation method. Instead of running the command in the terminal, copy only the tunnel token from the command. Now update your `docker-compose.yml` file to include the Cloudflare Tunnel configuration: ```yaml filename="docker-compose.yml" cloudflared: image: cloudflare/cloudflared:latest container_name: polar-cloudflared restart: unless-stopped command: tunnel --no-autoupdate run environment: - TUNNEL_TOKEN=your_tunnel_token ports: - "127.0.0.1:3001:3000" ``` Replace `your_tunnel_token` with the token you copied earlier. Now, your `docker-compose.yml` file should look like this: ```yaml filename="docker-compose.yml" services: polar-mcp: image: node:24 container_name: polar-mcp restart: unless-stopped command: > npx -y --package @polar-sh/sdk -- mcp start --access-token your_access_token --port 3000 --transport sse ports: - "127.0.0.1:3000:3000" # Only bind to localhost cloudflared: image: cloudflare/cloudflared:latest container_name: polar-cloudflared restart: unless-stopped command: tunnel --no-autoupdate run environment: - TUNNEL_TOKEN=your_tunnel_token ports: - "127.0.0.1:3001:3000" ``` Restart the Docker containers: ```bash docker compose down docker compose up -d ``` In Cloudflare, your tunnel should now show up as a connector. Click **Next**. ### Configuring the tunnel On the **Route Traffic** page, configure the tunnel using the details below. 1. **Hostname**: Enter the subdomain you chose earlier (for example, `polar-mcp.yourdomain.com`). 2. **Service**: Enter the URL of your MCP server (for example, `http://polar-mcp:3000`). 3. Click **Save tunnel**. ### Testing your secure MCP setup Now it's time to test whether everything is working correctly. Open a browser and navigate to: ``` https://polar-mcp.yourdomain.com/sse ``` You should be prompted to authenticate through Cloudflare Access. ![A screenshot shows the Cloudflare Access login screen with a header showing the Cloudflare Access logo. The main panel displays a form titled "Get a login code emailed to you" with an email input field and a blue "Send me a code" button. Below the form is the hostname "cloudflareaccess.com" and "Polar MCP" text. At the bottom of the screen, there's a Cloudflare Zero Trust logo.](/assets/mcp/remote-servers/cloudflare-access-auth.png) After logging in, you should see a response similar to the one you got when testing the local server. ### Using Cloudflare Warp to connect to the MCP server In Cloudflare Zero Trust: 1. Navigate to **Gateway**. 2. Click on **Add a device**. 3. Follow the instructions to install Cloudflare Warp on your machine. Open the Cloudflare Warp app and navigate to the **Preferences** > **Account** tab. ![A screenshot of Cloudflare WARP preferences window shows the Account tab selected. The interface displays a license key, a section labeled Devices using your license key showing macOS (This device) Mac15,12, and a Login to Cloudflare Zero Trust button at the bottom.](/assets/mcp/remote-servers/cloudflare-warp.png) Click the **Login to Cloudflare Zero Trust** button. This will show a modal with a text field, where you enter your team name. Your team name is the customizable portion of your team domain. You can view your team name in Zero Trust under **Settings** > **Custom Pages**. Enter your team name and click **Continue**. This will open a browser window to authenticate with Cloudflare Zero Trust. Once authenticated, you should see a message indicating that your device is connected to Cloudflare Zero Trust. If you encounter connection issues, you may need to disable Warp's DNS features. To do so, navigate to your Cloudflare Zero Trust dashboard and go to **Settings** > **Warp Client** > **Profile settings**. Select **Configure** from the three-dot menu option for your profile, and under **Service mode**, select **Secure Web Gateway without DNS Filtering**. Once Warp is connected, restart your `polar-mcp` and `cloudflared` Docker containers so that the `cloudflared` routes tunnel correctly. ### Adding Warp access to the MCP server 1. In the Cloudflare Zero Trust dashboard, navigate to **Settings** > **Warp Client**. 2. Under **Device enrollment permissions**, click **Manage**. 3. Click **Login methods**. 4. Activate **WARP authentication identity**. 5. Activate **Apply to all Access applications**. 6. Click **Save**. ### Testing the MCP connection To test the MCP connection, you can use the MCP Inspector we used earlier: ```bash pnpx @modelcontextprotocol/inspector@0.11.0 ``` In the MCP Inspector: 1. Select the SSE transport. 2. Enter your MCP server URL, `https://polar-mcp.yourdomain.com/sse`. 3. Click **Connect**. You'll be redirected to the Cloudflare Access login page. After authentication, the MCP Inspector should connect to your Polar MCP server securely. ![The MCP Inspector interface shows a successful connection to the Polar MCP server. The main panel displays a Connected status with a green indicator. The server URL is https://polar-mcp.redirectmap.com/sse. The right panel shows server metadata, including the name Polar MCP, version 0.1.0, and capabilities like tools, resources, prompts, and sampling support.](/assets/mcp/remote-servers/polar-mcp-connected.png) With this in place, you can enroll your team members in Cloudflare Zero Trust and provide them with access to the MCP server. They can use any MCP client to connect to the server, and all traffic will be encrypted and authenticated through Cloudflare. ## Comparing deployment models
Both deployment models follow the MCP Specification, allowing you to choose the approach that best fits your needs without changing client-side integrations. ## Other deployment models We experimented with several other deployment models, including: ### AWS Lambda AWS Lambda is similar to Cloudflare Workers, but managing the AWS environment and IAM roles is more complex. We used the [Run Model Context Protocol (MCP) servers with AWS Lambda](https://github.com/awslabs/run-model-context-protocol-servers-with-aws-lambda) example as a starting point, but ran into a dead end when it came to the SSE transport. AWS open-sourced a [server adapter](https://github.com/awslabs/run-model-context-protocol-servers-with-aws-lambda/blob/main/src/typescript/src/server-adapter/index.ts) for the MCP servers, which simplifies running `stdio` MCP servers as Lambda functions. However, the adapter depends on a custom transport that no clients currently support. AWS notes that with upcoming changes to the MCP Specification, this may change. For now, we recommend using Cloudflare Workers or an on-premises deployment. ### Container-based deployment with Docker and Kubernetes This was our initial approach, but we found it more complex than necessary for most use cases. While Docker and Kubernetes provide great flexibility and scalability, they also introduce additional overhead in terms of setup and maintenance. We recommend using Docker for local development and testing, but Kubernetes is often overkill for production deployments unless you have a specific need for it. A Kubernetes deployment may suit your needs if you provide MCP servers as a service to multiple teams or customers. In that case, you can use Kubernetes to manage scaling, resource allocation, and isolation between different MCP servers. ## In summary When deploying remote MCP servers, start with the server's transport mechanism and hosting requirements. If it needs to be publicly accessible, Cloudflare Workers is a great option. If you need to access internal data or have specific hosting requirements, consider using Cloudflare Tunnel with Zero Trust (or a VPN) to expose your server securely. Then make sure you understand the security implications of your deployment model. For example, if you're using Cloudflare Workers, ensure that your server is properly configured to handle authentication and authorization. If you're using Cloudflare Tunnel, you have to ensure that your server is properly secured and that only authorized users can access it. Finally, consider the trade-offs between different deployment models. Cloudflare Workers is a great option for quick deployments with minimal maintenance overhead, while on-premises deployments give you more control over the environment but require more management. As MCP continues to evolve, these deployment patterns will adapt, but the core principles of secure, accessible MCP servers will remain relevant regardless of the transport mechanisms chosen by the specification. If you have any questions or feedback about this guide, or want to share your own experiences with deploying remote MCP servers, [please join us on Slack](https://go.speakeasy.com/slack). To find out how we can help you generate your own MCP servers, [get in touch](https://www.speakeasy.com/contact) on our website. # Distribute your MCP server Source: https://speakeasy.com/mcp/distributing-mcp-servers Once you've built an MCP server, you need to decide how to distribute it to users. The four most common ways to expose an MCP server are: - As an open-source project - As an npm package - As an installable MCPB file - As a remote server Which of these methods you select depends on two factors: - **Your users:** Are they technical developers or non-technical business users? - **Your resources:** Can you maintain and pay for a hosted service? If your users are technical, distribute your MCP server as an open-source project that developers can build themselves or as an npm package for easier installation. Open-source projects require you to provide build instructions and are best suited for technical users. MCP servers built using JavaScript or TypeScript can be packaged with npm. For users, this is more convenient than building a project locally but still requires writing some configuration. If your users are non-technical, and you can't maintain a hosted server, use MCPB packaging. MCPB (previously called DXT) creates a desktop installer that users can install in Claude Desktop with minimal steps. If you have the financial and developer resources for hosting, deploy your MCP server remotely. Although remote servers cost more, the minimal configuration for users and easy integration for developers building automated systems means that they provide the best user experience. ![Flowchart for selecting an option](/assets/mcp/distributing-your-mcp-server/mcp-distribution-flowchart.png) Choosing the right distribution method is critical to the successful adoption of your MCP server. This guide explores when best to use each method and demonstrates how to implement it using an example Echo MCP server. ## Prerequisites To follow this tutorial, you need: - A working understanding of [MCP transports](/mcp/building-servers/protocol-reference/transports) and how the MCP server receives inputs and sends outputs - Node.js 22 or later - A clone of the simple MCP Echo server, which demonstrates all distribution methods without complex dependencies Install the MCP Echo server as follows: ```bash git clone https://github.com/speakeasy-api/examples.git cd mcp-echo-input-server ``` ## Distribute your MCP server as an open-source project The simplest option is to release your MCP server as an open-source project and provide build and installation instructions in a README. This works when: - Your users are technical developers comfortable with command-line tools - You don't have resources for maintaining a remote server - You're starting out and would benefit from community input and contributions Once the project matures, consider deploying your MCP server as an npm package or MCPB extension so that you can reach non-technical users. ### Upload your MCP server to an open-source repository First, upload your project to an artifact repository platform like [GitHub](https://github.com/). Then, create a README that provides your users with build steps and configuration details for their AI tools, similar to the instructions in the example Echo MCP server README below. Note that the `mcp-echo-input-server` project uses plain JavaScript, so no build step is needed. If your project uses TypeScript, add a build step before installing the dependencies. For example: > Build the project with the following command: > > ```bash > npm run build > ``` ### Example README for the Echo MCP server Install all the project dependencies: ```bash npm install ``` The entry point file is `index.js`. Add the following MCP server configuration to Claude Desktop, via **Settings -> Developer -> Edit Config**, after replacing `` and `` with your actual username and the absolute path to the project: ```json { "mcpServers": { "echo-server-local-build": { "command": "node", "args": ["/mcp-echo-input-server/index.js"], "env": { "USER_NAME": "" } } } } ``` Reload Claude Desktop, open a new conversation and enter the following prompt: ```txt Hi Claude. Use the MCP Echo server to echo this: "This is a working MCP server as a local build package" ``` You will receive an output similar to the following: ![Testing local build](/assets/mcp/distributing-your-mcp-server/local-build-test-result.png) ## Distribute your MCP server as an npm package If you built your MCP project with JavaScript or TypeScript, you can publish your MCP server as an npm package. This works when: - Your users are technical developers who can handle npm installations - You don't have the resources to host and maintain a remote server If you're creating an MCP server that exposes API capabilities and you have an OpenAPI document, you can use [Speakeasy](/docs/standalone-mcp/build-server) to generate a TypeScript MCP server ready for deployment and distribution. ### Prepare your MCP server for packaging Install the project dependencies: ```bash npm install ``` Next, make sure the `package.json` file is ready for packaging. Add a name, description, author name, author email address, keywords, entry point (main), and the CLI command `bin` to run the project: ```json { "name": "mcp-echo-input-server", // ... "description": "A simple Model Context Protocol (MCP) server with an echo tool that returns user input with customizable user name", "main": "index.js", "bin": { "mcp-echo-server": "./index.js" }, // ... "keywords": ["mcp", "model-context-protocol"], "author": { "name": "", "email": "" } // ... } ``` ### Check that your package name is available The npm registry requires unique package names. You need to check whether your package name is available before publishing. First, ensure that you're logged in: ```bash npm login ``` Then run the following command with the name of your project in the `package.json` file. ```bash npm view ``` If it returns a `404` error, proceed with the rest of the commands. Otherwise, try other combinations until you find an unused name, then update the name in the `package.json` file, and proceed. ### Publish your MCP server as an npm package Package and publish your npm package: ```bash npm publish ``` When you publish the package, include documentation that provides users with the instructions for installing the MCP server in Claude Desktop and the configuration code they need to add to their `claude_desktop_config.json` files. ### Example documentation for the Echo MCP server Add the following configuration to your `claude_desktop_config.json` file: ```json { "mcpServers": { "echo-server": { "command": "npx", "args": ["-y", ""], "env": { "USER_NAME": "" } } } } ``` Reload Claude Desktop. You should now see the MCP server in the chat. Enable it using the toggle button next to **echo-server**. ![Selecting NPM built package](/assets/mcp/distributing-your-mcp-server/npm-package-selection.png) Enter the following prompt to test the MCP server functionality: ```txt Hi Claude. Use the MCP Echo server to echo this: "This is a working MCP server as an npm package." ``` Claude should return a similar response to the following: ![Test results](/assets/mcp/distributing-your-mcp-server/npm-package-test-result.png) ### Automate releases To make release and distribution easier, consider using CI/CD tools, such as [this GitHub Action](https://github.com/speakeasy-api/examples/blob/main/optimizing-your-mcp-openapi/sdk/racing-lap-counter-typescript/.github/workflows/sdk_publish.yaml), to automate the npm release process. ## Distribute your MCP server as an MCPB file [MCP Bundles (MCPB)](https://github.com/anthropics/mcpb), formerly known as Desktop Extensions (DXT), is a zip archive extension developed by Anthropic to make MCP server installation easier with one click. An MCPB package contains the local MCP server and a `manifest.json` file describing its capabilities. > **Note:** > If you previously used DXT, consult the migration instructions in the MCPB repository [README](https://github.com/anthropics/mcpb/blob/main/README.md). MCPB is useful when: - **You're targeting non-technical users who need your MCP server but shouldn't have to handle npm installations or dependency management:** For example, business users, marketing teams, or domain experts - **You want distribution without infrastructure costs:** Local installation means no hosting, scaling, or maintenance overhead on your end, while still providing a professional installation experience - **You're building cross-platform tools:** MCPB packages work across operating systems and can bundle servers written in any language (such as Python, Node.js, or Go), as long as they implement the MCP protocol ### Package your MCP server as an MCPB file First, install the project dependencies: ```bash npm install ``` Then install the `@anthropic-ai/mcpb` package, which contains a CLI that helps with the creation of both the `manifest.json` and the final `.mcpb` file: ```bash npm install --dev @anthropic-ai/mcpb ``` Although the MCPB documentation suggests installing `@anthropic-ai/mcpb` globally, you can install it as a dev dependency in your projects to maintain consistent versions. Create a `manifest.json` file by running the following interactive command and answering the configuration prompts that it returns: ```bash npx @anthropic-ai/mcpb init ``` When prompted, enter values for fields such as the author name, package name, and description, and add the following user-configurable options, which you'll use for the `USER_NAME` environment variable: ![Configuring the Manifest file](/assets/mcp/distributing-your-mcp-server/mcpb-manifest-configuration.png) At the time of writing, a bug prevents Claude from recognizing the `manifest_version` key in the `manifest.json` file. Resolve this issue by replacing `manifest_version` with `dxt_version` in the generated `manifest.json` file: ```diff { - "manifest_version": "0.1", + "dxt_version": "0.1", ... } ``` In the same `manifest.json` file, edit the `USER_NAME` variable to point to the `userName` user-configurable option: ```json { "server": { // ... "env": { "USER_NAME": "${user_config.userName}" } } } ``` Next, create the distributable MCP file by running the `pack` command: ```bash npx @anthropic-ai/mcpb pack ``` ### Test the MCPB file Find the generated `mcp-echo-input-server.mcpb` file in the project directory. Let's open the file with Claude and install the MCP server. Navigate to the Claude Desktop **Settings**, open the **Extensions** screen, and click the **Advanced settings** button. ![Advanced settings button](/assets/mcp/distributing-your-mcp-server/claude-advanced-settings.png) In the advanced settings for **All extensions**, scroll to the bottom of the page and click the **Install Extension...** button. Browse for and select the `mcp-echo-input-server.mcpb` file. When Claude displays a modal with the extension description, confirm the installation by clicking **Install**. ![Adding the MCP server in Claude](/assets/mcp/distributing-your-mcp-server/mcpb-installation-modal.png) In the **Configure** dialog, enter a username value, such as your name, to be used as an environment variable for the project. ![Username input dialog](/assets/mcp/distributing-your-mcp-server/mcpb-username-input.png) **Save** the changes and enable the extension using the toggle button. ![Enable extension](/assets/mcp/distributing-your-mcp-server/toggle-enabled.png) Open a new conversation in Claude, ensuring that the MCP server is activated. ![Selecting MCP Echo input server](/assets/mcp/distributing-your-mcp-server/mcpb-server-selection.png) Test the MCP server by prompting Claude to use it: ```txt Hi Claude. Use the MCP Echo server to echo this. "This is a working MCPB server". ``` You should receive a similar output to the following: ![Testing the MCP server](/assets/mcp/distributing-your-mcp-server/mcpb-test-result.png) > **Note:** > You may encounter Claude alerts about typing issues. Ignore them by clicking the **X** button or prevent them by typing your projects appropriately. ### Upload the MCPB file You now have a working `.mcpb` file. Upload it to an artifact repository so that users can download and install your MCP server. You can also add it to your CI/CD pipeline using [this GitHub Action](https://github.com/speakeasy-api/examples/tree/main/mcp-echo-input-server/.github/workflows/mcpb-release.yml), which automatically builds and publishes your `.mcpb` file to the GitHub repository when there is a new release. ## Distribute your MCP server remotely If you have the resources to maintain a remote server, distributing your MCP server remotely offers the best solution from both technical and business perspectives. ### Benefits of remote distribution Remote distribution allows you to do the following: - Monitor, log, and track tool calls, authentication, and user activity. - Implement business models for monetizing your MCP server. - Maintain control over data in sensitive fields. - Simplify installation for Claude Desktop users who can add remote servers via Claude Connectors. ![Connectors](/assets/mcp/distributing-your-mcp-server/claude-connectors.png) If your server requires authentication and your users are technical, provide them with the necessary documentation for entering OAuth configurations. If your users aren't technical, use the **Connect** button to redirect them to an OAuth authorization page that grants them access to your API or services. - Provide integration options for developers building automated workflows with the Anthropic SDK and OpenAI Agents SDK. ### Build the server If you're exposing your API capabilities through MCP tools, you have two options: - Build the infrastructure yourself and handle architecture and maintenance. - Use [Gram](https://getgram.ai), the platform we developed for creating, hosting, and distributing MCP servers with just an OpenAPI document. ### Choose your hosting platform You can use traditional cloud servers, such as AWS, GCP, and Azure, to deploy your MCP server remotely. However, this option requires you to handle server setup, configure SSL certificates, and manage server configuration yourself. If you've created your server using [Gram](/docs/gram/host-mcp/deploy-mcp-server), we recommend hosting it there for a more streamlined experience. Other MCP server creation platforms have similar hosting and deployment solutions, such as [FastMCP Cloud](https://gofastmcp.com/deployment/fastmcp-cloud). ### Set up OAuth authentication Implement OAuth 2.1 to secure your remote MCP server: - For Gram-hosted servers, follow the guide to [OAuth implementation with Gram](/docs/gram/examples/oauth-external-server). - For self-hosted servers, follow the guide to [general OAuth implementation for MCP](/docs/gram/host-mcp/adding-oauth). ### Distribute to users Based on your security requirements, decide whether your server will be public or private: - **Private servers** work best for APIs used by enterprise teams, such as for internal marketing tools. You handle user configuration variables on the server side and tightly control access. - **Public servers** work best for APIs requiring broader distribution, such as for SaaS, but they require solid OAuth authentication. Although users set their own configuration variables, secure authorization flows are still necessary. ## Final thoughts In this guide, we've explored how to distribute your MCP server with different distribution options. MCP server distribution strategies depend on target users and available resources. Follow the recommendations below according to your use case: - **For local installations**, provide multiple distribution options like MCPB, local build, and npm packages. This gives developers alternatives if they encounter issues with any single method. MCPB is well-maintained, so non-technical users will rarely have problems. - **In your documentation**, provide configuration examples for popular AI agents like Claude, Cline, Cursor, and VS Code. - **Publish your MCP server** on the official Anthropic MCP registry. If you're using Gram, consult our guide to referencing your MCP server in the registry. # Building an MCP Server with FastAPI and FastMCP Source: https://speakeasy.com/mcp/framework-guides/building-fastapi-server If you want to make your FastAPI application work with Claude, Cursor, and other AI agents, you need an MCP server. But building one from scratch is tedious when you already have routes and schemas in place. FastMCP and Speakeasy can both generate an MCP server directly from an existing API without rewriting logic or duplicating schemas, making your backend instantly agent-ready. This article shows you how to build an MCP server for a FastAPI application using either [FastMCP](https://gofastmcp.com/getting-started/welcome) or [Speakeasy](https://www.speakeasy.com/), and compares the tools in terms of ease of setup, visibility into generated code, and the long-term maintainability of your MCP server. ## Setting up the example project We'll build an MCP server with each tool using the [APItizing Burgers example project](https://github.com/speakeasy-api/examples/tree/main/framework-fastapi). Start by cloning the project with the following command: ```bash git clone https://github.com/speakeasy-api/examples cd examples/framework-fastapi ``` To make the setup easier, we'll use the `uv` environment manager. You can use any Python environment creation tool you're familiar with. ```bash uv venv source .venv/bin/activate ``` Next, install the required dependencies for the project. The `apitizing-burger` project is already set up with FastAPI, so install the FastAPI package and its dependencies: ```bash uv pip install "fastapi[all]" ``` Now run the API: ```bash uvicorn app.main:server --reload ``` It's a good idea to have [Claude Desktop](https://claude.ai/download) installed on your machine to test the MCP servers. ## Building an MCP server with FastAPI and FastMCP FastMCP is a Python package that provides a high-level implementation of the MCP Python SDK. You can use FastMCP to quickly build an MCP server without worrying about low-level implementation details, like managing component lifecycles, defining tools, handling resources, or configuring prompts. Through its [FastAPI integration](https://gofastmcp.com/servers/openapi#fastapi-integration), FastMCP automatically transforms your routes into MCP tools. ### 1. Installing FastMCP First, install the [FastMCP package](https://github.com/jlowin/fastmcp?tab=readme-ov-file#installation): ```bash uv pip install fastmcp ``` ### 2. Creating the MCP server file Create a file in the `app` directory called `mcp_server.py`: ```bash cd app/ && touch mcp_server.py ``` ### 3. Creating the MCP server Add the code below to the `mcp_server.py` file: ```python from fastmcp import FastMCP from app.main import server # Create an MCP server from your FastAPI app mcp = FastMCP.from_fastapi(app=server) if __name__ == "__main__": mcp.run() ``` This code first registers your FastAPI app with FastMCP. Then, FastMCP inspects the app, finds the declared routes in `app/main.py`, and turns each route into an MCP tool without needing any additional configuration. ### 4. Adding the MCP server to Claude desktop There is no need to write the configuration to add the MCP server to Claude Desktop. Navigate to the `app` directory and run the following command: ```bash fastmcp install mcp_server.py ``` FastMCP will return output similar to below, confirming that your server has been installed in Claude. ![FastMCP Installing MCP server](/assets/mcp/building-fastapi-server/fastmcp-installing-mcp-server.png) However, the MCP server won't work yet because the configuration is missing the `fastapi[all]` dependency and the Python path environment variable. Let's fix that now. In Claude Desktop, go to **Settings -> Developer -> Edit config in Claude Desktop** and open the `claude-desktop-config.json` file. Update the generated configuration in `claude-desktop-config.json` to match the following: ```json { "mcpServers": { "mcp_server": { "command": "uv", "args": [ "run", "--with", "fastmcp", "--with", "fastapi[all]", "fastmcp", "run", "path/to/project/fastmcp-speakeasy-project/base/app/mcp_server.py" ], "env": { "PYTHONPATH": "path/to/project/fastmcp-speakeasy-project/base/" } } } } ``` This updated configuration adds the `fastapi[all]` dependency and the `PYTHONPATH` environment variable. The integration of FastAPI and FastMCP is now complete, and you can test the MCP server in Claude Desktop. ### Testing the integration In Claude Desktop, click **Search and tools**. ![Clicking on the search and tools button](/assets/mcp/building-fastapi-server/mcp-server-search-and-tools-button.png) Your configured MCP servers will appear in the menu that opens. ![Selecting the MCP server](/assets/mcp/building-fastapi-server/mcp-server-fastmcp-claude.png) Your MCP server will appear as `mcp_server`. Click on it to see the tools that have been added. ![Listing the tools](/assets/mcp/building-fastapi-server/mcp-server-fastmcp-claude-listing-tools.png) These tools correspond to your FastAPI routes and can now be used by Claude. Now test the MCP server by asking Claude to create a burger. As shown in the screenshot below, Claude successfully calls your API to create a burger. ![Creating a burger](/assets/mcp/building-fastapi-server/claude-creating-a-burger.png) ### More configuration with FastMCP FastMCP gives you some flexibility in how tools are exposed. You can customize the server name, set timeout values, rename tools, and control which endpoints are included or excluded. For example, the code below renames the server, overrides a tool name, and excludes the route for deleting a burger: ```python mcp = FastMCP.from_fastapi( app=server, name="Apitizer MCP Server", timeout=5.0, mcp_names={"createBurger": "Create a burger menu"}, route_maps=[ # Exclude delete burger route RouteMap(methods="DELETE", pattern=r".*", mcp_type=MCPType.EXCLUDE, tags={"burger"}), ], ) ``` ### Considerations for FastMCP in FastAPI FastMCP avoids code duplication by reusing your existing FastAPI endpoints. Typing and schema definitions are preserved, including inheritance and Pydantic validations. This preservation also extends to dependencies, middleware, and authentication, all of which carry over into the MCP layer. FastMCP behaves like a black box. You get speed and simplicity, but not much visibility into how tools, prompts, or resources are constructed. In contrast, when you use the MCP Python SDK directly, you build things manually but retain full control. You can interact with the component lifecycle, adjust behavior, and trace execution more easily. Without visibility, debugging tool behavior can be difficult when something breaks or doesn't work as expected. ## Building an MCP server with FastAPI and Speakeasy [Speakeasy](https://www.speakeasy.com/) generates SDKs in multiple languages, documentation, Terraform providers, and MCP servers from OpenAPI documents. Speakeasy's support for [generating MCP servers](https://www.speakeasy.com/docs/standalone-mcp/build-server) is currently limited to TypeScript SDKs, with additional language support coming soon. All you need is an OpenAPI document and a [Speakeasy account](https://www.speakeasy.com/docs/introduction#sign-up). ### 1. Installing the Speakeasy CLI First, install the [Speakeasy CLI](https://www.speakeasy.com/docs/introduction#install-the-speakeasy-cli) on your machine. On macOS or Linux: ```bash # Homebrew (macOS) brew install speakeasy-api/tap/speakeasy # or script Installation (macOS and Linux) curl -fsSL https://go.speakeasy.com/cli-install.sh | sh ``` On Windows: ```bash # Windows Installation # Using winget: winget install speakeasy # or Using Chocolatey: choco install speakeasy ``` ### 2. Uploading the OpenAPI document The APItizing Burgers project already has `openapi.yaml` and `openapi.json` OpenAPI documents. If you're building an MCP server from an existing FastAPI project, you might need to generate these files. Take a look at our [tutorial on generating an OpenAPI document with FastAPI](/openapi/frameworks/fastapi#basic-fastapi-setup) for step-by-step instructions. To start the process, run the following command from the project's root directory: ```bash speakeasy quickstart ``` You'll be prompted to authenticate with Speakeasy and then enter the path to the OpenAPI document. Enter `./openapi.yaml`. ![OpenAPI document path](/assets/mcp/building-fastapi-server/speakeasy-openapi-path.png) ### 3. Naming the SDK Name the SDK `mcp-burger-sdk`. ![Choosing the SDK name](/assets/mcp/building-fastapi-server/speakeasy-choosing-sdk-name.png) ### 4. Selecting the output Speakeasy will ask what you want to generate. Choose **Model Context Protocol (MCP) Server**, and then the sub-option **TypeScript SDK with Server**. ![Choosing the SDK type](/assets/mcp/building-fastapi-server/speakeasy-choosing-sdk-type.png) Speakeasy will ask you where to save the generated SDK and what to name the npm package. Press **Enter** to use the current directory and the default package name, or modify them. Speakeasy will generate a TypeScript SDK in the `mcp-burger-sdk-typescript` directory. The MCP server code will be located at `mcp-burger-sdk-typescript/src/mcp-server`, with a built version at `mcp-burger-sdk-typescript/bin/mcp-server.js` (this is the file you'll reference in your Claude Desktop configuration). ### 5. Adding the MCP server to Claude desktop Now configure the MCP server in the `claude-desktop-config.json` file: ```json { "mcpServers": { "McpBurgerSDK": { "command": "node", "args": [ "path/to/mcp-burger-sdk-typescript/bin/mcp-server.js", "start" ] } } } ``` Reload Claude Desktop and you should see the server and its available tools. ![Viewing the MCP server](/assets/mcp/building-fastapi-server/mcp-typescript-sdk.png) You now have a working MCP server generated with Speakeasy. ### More configuration with Speakeasy With Speakeasy, you can [customize your MCP server](/docs/standalone-mcp/customize-tools) using the `x-speakeasy-mcp` extension in your OpenAPI document. For example, to disable the generation of a tool for an operation: ```yaml paths: /products: post: operationId: createProduct tags: [products] summary: Create a product description: API endpoint for creating a product in the CMS x-speakeasy-mcp: disabled: true name: create-product scopes: [products, create, ecommerce] description: | Creates a new product using the provided form. The product name should not contain any special characters or harmful words. ``` In this code configuration, `disabled: true` specifies that the tool should be ignored. You can use `name` to change the name of the MCP tool, or use `scopes` to tag tools, which allows you to run the MCP server with only specific groups of tools. When you start the MCP server with Speakeasy, you can specify which scopes to include with the `--scope` flag: ```json { "mcpServers": { "EcommerceSDK": { "command": "npx", "args": [ "-y", "--package", "e-commerce-sdk", "mcp", "start", "--scope", "products" ], "env": { "API_TOKEN": "your-api-token-here" } } } } ``` If you can't or don't want to modify an OpenAPI document, Speakeasy [overlays](https://www.speakeasy.com/openapi/overlays) provide a way to use the `x-speakeasy-mcp` extension without changing the original file, for example: ```yaml overlay: 1.0.0 info: title: Add MCP scopes version: 0.0.0 actions: - target: $.paths.*["post","head","query"] update: { "x-speakeasy-mcp": { "scopes": ["products"] } } ``` The code above instructs Speakeasy to generate tools for operations with the tag `products` and HTTP operations of types `POST`, `HEAD`, and `QUERY`. ## FastMCP vs Speakeasy: Which tool should you choose? FastMCP and Speakeasy offer two different paths to building an MCP server from your FastAPI application. The right choice depends on your priorities around development speed, code transparency, and long-term maintenance needs. ### Initial setup and configuration **FastMCP** gets you running quickly by reusing your existing FastAPI app with minimal configuration. If your routes are typed and documented, FastMCP can expose them as tools with almost no additional work. **Speakeasy** requires an OpenAPI document and an interactive CLI setup process to generate an MCP server. While this takes more time initially, it generates a clean, extensible SDK with a built-in MCP server that can integrate with Claude and Cursor, or run as a standalone service. ### Visibility of generated code **FastMCP** takes the opposite approach: You don't see how tools are constructed. FastMCP automatically converts your FastAPI routes into MCP tools, but hides the internal logic, prompt structure, and resource handling. This makes debugging harder as your server grows. **Speakeasy** generates explicit code for the server and tools in the `mcp-server` directory. You can inspect how each tool is defined, see the input and output mappings, and modify behavior as needed. This helps when you want to understand or extend how the MCP layer works. ### Customization options **FastMCP** provides basic customization options. You can rename tools, set timeouts, or exclude routes using configuration options, but you're limited to the configuration parameters the library exposes. **Speakeasy** allows for extensive customization, whether directly in the OpenAPI document or using overlays to define external configuration. You can change tool names, disable endpoints, apply scopes for selective tool groups, and modify descriptions, all without touching application code. ### Long-term maintenance considerations **FastMCP** tightly couples your MCP server to your FastAPI app. Changes to the application's route structure will automatically update the available MCP tools, which simplifies maintenance for smaller projects but can create complexity as the API evolves. **Speakeasy** separates the MCP server from application logic using the OpenAPI document, which serves as the single source of truth. Changes to your API require regenerating the MCP server from the updated specification, but the server remains independent of your application's internal structure. ### Distribution methods **FastMCP** is designed for local development workflows within Claude Desktop. While effective for personal tools and rapid prototyping, it doesn't provide built-in mechanisms for packaging or distributing MCP servers to other environments. **Speakeasy** generates MCP servers that can be published and distributed as npm packages or used directly with `npx`. This allows you to share tools across teams, deploy to different environments, and distribute to external users. ## Next steps Having compared FastMCP and Speakeasy for generating MCP servers, here's how to choose the right tool for your project: - Choose **FastMCP** if you don't have an OpenAPI document or don't plan to deploy and maintain the MCP server in the long term. FastMCP is a quick way to turn your existing routes into tools with minimal complexity, making it useful for exploring the MCP ecosystem or rapid prototyping. - Choose **Speakeasy** if you have an OpenAPI document, plan to maintain the server over time, or want to share it with others. While Speakeasy requires more initial setup, it gives you more control, visibility, and customization. Speakeasy also works well for prototyping, especially when you want to build something fast while keeping a path open to production. # Model Context Protocol (MCP) Hub Source: https://speakeasy.com/mcp import { CardGrid } from "@/components/card-grid"; import { coreConceptsCards, toolDesignCards, useCasesCards, gettingStartedCards, buildingServersCards, introToMcpCards, } from "@/lib/data/mcp-index"; Welcome to the Model Context Protocol Hub. MCP is an open standard that enables AI agents to securely connect to external tools, data sources, and services. Whether you're using MCP servers or building your own, this hub will help you get the most out of the protocol. ## Intro to MCP Comprehensive guides to understanding MCP in the context of different AI ecosystems and platforms. ## Core concepts Understanding MCP's building blocks is essential for making informed decisions about how to use it effectively. ## Tool design Building effective MCP servers requires thoughtful design. Learn how to create tools that agents can use efficiently. ## Building MCP servers Create and deploy your own MCP servers with comprehensive guides covering protocol implementation, security, and framework-specific patterns. ## Use cases See how companies are using MCP in production and explore practical implementation patterns. --- ## Getting started New to MCP? Start with these resources to understand what it is and how to use it. # Why MCP is useful: An introduction to MCP for skeptics Source: https://speakeasy.com/mcp/mcp-for-skeptics import { CardGrid } from "@/components/card-grid"; You're right to be skeptical about the [Model Context Protocol (MCP)](https://modelcontextprotocol.io). From the outside, it looks like word salad, and it arrived on the heels of the hyped-up AI darlings known as "vibe coding" and "agentic". If you've dared to poke around, you're probably left with the feeling that it's a Rube Goldberg machine, and everyone's either in on the joke or dazzled by the complexity. Developers love shiny complexity, but anyone who lived through the early internet's immature, brittle protocols and torturous RPC standards knows to hit the brakes when YouTube talking heads sing a new protocol's praises in unison. This skepticism and care shouldn't end at MCP, though. The entire generative AI ecosystem is rife with complexity and potential pitfalls. To quote the sagely Samuel Colvin (of [Pydantic](https://docs.pydantic.dev/latest/)): > From a software engineer's point of view, you can think of LLMs as the worst database you've ever heard of, but worse. If LLMs weren't so bloody useful, we'd never touch them. Well, if MCP servers weren't so bloody useful, we'd never touch them either! We believe MCP will eventually enable powerful AI agent ecosystems, but we're not here to evangelize. The protocol adds genuine complexity and operational overhead that isn't justified for many use cases. Function calls or REST APIs are often the simpler, better choice. But there are specific scenarios where MCP's architecture genuinely shines - where its standardized discovery, stateful sessions, and cross-platform compatibility solve problems that are painful with alternatives. MCP servers like [Desktop Commander](https://desktopcommander.app/) and [Playwright MCP](https://github.com/microsoft/playwright-mcp) demonstrate this: they turn any LLM into a powerful automation tool for desktop and web interactions that would require significant custom development otherwise. The question isn't whether MCP is inherently good or bad - it's whether its benefits justify the complexity for your specific needs. ## What people think about MCP > This has made a lot of people very angry and been widely regarded as a bad move. > > - Douglas Adams Polarized doesn't begin to describe the reaction to MCP on the internet. The extremes appear to fall into four clear camps: 1. **The "everything about this sucks" naysayers.** Here are a few highlights from our favorite orange site: > MCP is a kitchen sink of anti-patterns. There's no way it's not forgotten in a year, just like Langchain will be. > It's a half-baked, rushed out, speculative attempt to capture developer mindshare and establish an ecosystem/moat early in a (perceived) market. 2. **The deliberately obtuse.** I won't quote these commenters for being ignorant, but you know the people who say, "I can't get my head around this, so it must be useless." I believe some of this group's resistance is driven by the viscerally negative reaction of the naysayers. This one includes the "Isn't this just an OpenAPI spec?", "Why not just use REST?", and "I have yet to see one compelling use case" crowds. 3. **The builders who use MCP to get things done _now_ or help fix what's broken in the spec.** Think of David Cramer learning MCP while building the [Sentry MCP server](https://mcp.sentry.dev/) in public - or Samuel Colvin, Armin Ronacher, the developers from Cloudflare, LangChain, Vercel, and others enthusiastically (and in record time) helping to [review updates to the MCP Specification](https://github.com/modelcontextprotocol/modelcontextprotocol/pull/206). This includes the hackers (the best kind) and tinkerers who built more than [15,000 MCP servers](https://mcp.so/) over the past six to eight months. 4. **Vendors and tooling companies.** We're in this group - we build a platform that generates MCP servers from existing APIs. This gives us a front-row seat to both MCP's potential and its implementation challenges, which we'll share honestly. We have a biased perspective, of course, but that perspective comes from solving real problems developers face with MCP implementation. ## Explore the skeptic's guide We've organized this guide into focused articles that address specific concerns and topics. Start with the criticisms if you're still unconvinced, or jump to the technical introduction if you're ready to understand how it works. ## Our take We're not here to evangelize, but we do believe MCP solves real problems in specific scenarios. The protocol adds genuine complexity that isn't justified for every use case - sometimes a simple REST API or function call is the right answer. But when you need cross-platform tool sharing, stateful long-running operations, or dynamic runtime tool discovery, MCP's architecture provides solutions that are painful to implement with alternatives. The question isn't whether MCP is good or bad. It's whether the benefits justify the complexity for your specific needs. We hope this guide helps you make that decision. # Common Criticisms of MCP (And Why They Miss the Point) Source: https://speakeasy.com/mcp/mcp-for-skeptics/common-criticisms import { CardGrid } from "@/components/card-grid"; import Image from "next/image"; We get it. MCP is complex, and the spec is still evolving. But let's address some common criticisms head-on. I know this first one looks like a straw man, but we've seen it more than once: ## "But MCP is just another API wrapper" Yes, it's a wrapper. But the underlying API can be just about anything: a database, a web service, a file system, or even a custom tool. MCP servers wrap these APIs in a way that makes them predictable from the LLM client's perspective. The point is that client code is not tied to a specific API implementation. The problem with calling it "just" another API wrapper is that you ignore the benefits of the protocol's design: - **Standardized communication:** MCP defines a consistent way for clients to interact with servers, regardless of the underlying API. This means you can swap out servers without changing client code. - **Dynamic tool discovery:** When you connect Claude to a project management MCP server, it can discover available tools at runtime - "Oh, this workspace has custom fields for priority and sprint. I can filter by those." The LLM adapts to what's available rather than failing on hardcoded function calls. - **Bidirectional communication:** MCP servers can send messages back to the LLM client, allowing it to react to events in real time. For example, a database MCP server can notify the MCP client when a new record is added (via the MCP `notifications/resources/list_changed` message), allowing the LLM client to update context without needing to poll or re-query. - **Session management across tools:** An LLM helping with data analysis can maintain an authenticated session with your database, keep a Jupyter Notebook kernel running, and preserve Matplotlib figure states - all simultaneously. This benefit is most apparent when you consider an MCP server that wraps a web browser. The MCP server can maintain the state of the browser session, allowing the LLM to interact with web pages as if it were a human user. If you broaden your definition of "API" beyond web services, a protocol like MCP becomes essential. **In short:** We need a protocol that provides a consistent way to interact with tools and services, regardless of their underlying implementation. ❌ The definition of API, as used by critics of MCP, is too narrow and limited to web services. APIs vary too widely to be pigeonholed into a single category. ✅ MCP, as a wrapper, provides clients with a standardized interface, dynamic tool discovery, bidirectional communication, and session management across tools. ## "Why use MCP instead of REST, OpenAPI, or agents.json?" REST excels at CRUD operations, and it saved us from the horrors of RPC, so why are we reinventing the wheel, or worse, driving in reverse? The difference is that most of the time, agents are not doing CRUD operations. They often need to perform multistep actions on remote and local services combined, while maintaining state and context. For example, the [Playwright MCP server](https://github.com/microsoft/playwright-mcp) allows LLMs to interact with web pages as if they were human users. It enables complex interactions like filling out forms, clicking buttons, and navigating pages, all while maintaining context about the user's session. Achieving this without a stateful protocol like MCP would drive anyone to madness. This is simply not something you can do with REST or OpenAPI. We've seen the AGPL-licensed [agents.json Specification](https://github.com/wild-card-ai/agents-json) mentioned as an alternative to MCP, but it doesn't have nearly the same capabilities. For the same reasons that REST is not a good fit for complex interactions, agents.json falls short. agents.json only solves the problem of remote tool discovery and invocation, but it doesn't provide the session management, dynamic tool discovery, or bidirectional communication that MCP does. **In short:** The problems LLMs solve often require stateful interactions with tools and services, not just simple CRUD operations. ## "Why not just use function calls instead of MCP?" Native LLM function calling already enables tool use. Why add MCP? This is a common and completely valid question. Function calling is often much simpler to implement than MCP, but here's where the architectural differences matter: With function calls, all integration logic lives in the LLM client and extends both ways - to the LLM (with proprietary function call interfaces) and the external services. Want to add Slack integration? The LLM client needs Slack-specific authentication, error handling, rate limiting, and response parsing. Want database access? Add database drivers, connection pooling, and SQL validation to the LLM client. Every new integration bloats the LLM client with tool-specific code. Yes, you could abstract the detail away with a library, but we'd still end up with custom implementations for each service, LLM, and client combination. This leads to a tangled mess of code that is hard to maintain, test, and scale. With MCP, the MCP client speaks one protocol to any number of specialized servers. Adding Slack means deploying a Slack MCP server - no client changes required. It's a plugin architecture that separates concerns: The MCP client handles conversation, MCP servers handle tool execution, and the protocol manages communication between them. The architectural difference becomes clear when visualized: **Function calls:** All integration logic lives in the LLM client. Each new service requires client-side implementation. **MCP:** The MCP client speaks one protocol to multiple specialized servers. New integrations require no client changes. This isn't about capability - it's about managing complexity at scale. Function calls hide the complexity until it's too late. MCP acknowledges and contains it from the start. Consider the classic "What's the weather like in Paris?" example: With function calling, the LLM client would need to handle each LLM provider's function call interface, for example: Using Anthropic's Python SDK: ```python import anthropic client = anthropic.Anthropic() response = client.messages.create( model="claude-opus-4-20250514", max_tokens=1024, tools=[ { "name": "get_weather", "description": "Get the current weather in a given location", "input_schema": { "type": "object", "properties": { "location": { "type": "string", "description": "The city and state, e.g. San Francisco, CA", } }, "required": ["location"], }, } ], messages=[{"role": "user", "content": "What's the weather like in Paris?"}], ) print(response) ``` With OpenAI's Python SDK: ```python from openai import OpenAI import json client = OpenAI() response = client.responses.create( model="gpt-4.1", input=input_messages, tools=[ { "type": "function", "name": "get_weather", "description": "Get the current weather in a given location", "parameters": { "type": "object", "properties": { "location": { "type": "string" } }, "required": ["location"], "additionalProperties": False }, "strict": True } ], input=[{"role": "user", "content": "What's the weather like in Paris?"}] ) print(response) ``` Note the subtle differences in how each provider defines the function. The complexity of function calling grows with each new provider, and the LLM client becomes tightly coupled to specific implementations. This makes it hard to switch providers or reuse code across different LLMs. If you're only using one LLM provider in your application, and you don't expect to add more, then function calls are a perfectly valid choice. But if you want to build a portable, reusable client that can work with any LLM provider, MCP is the way to go. ## "MCP is just a bad, terrible, no-good excuse for a protocol" Yes, we've seen the hot takes. MCP is "over-engineered," "unnecessarily complex," and "solving problems that don't exist." One particularly spicy commenter called it "a kitchen sink of anti-patterns" destined to be forgotten within a year. Let's address the elephant in the room: MCP _is_ complex. It uses JSON-RPC 2.0 over `stdio`, SSE, or "Streamable HTTP". It has its own transport negotiation, capability advertisement, and session management. If you squint, it looks like someone reinvented the Language Server Protocol (LSP) but made it worse. Could MCP have used REST with webhooks? Sure, if they were all hosted publicly. Could it have been "HTTP" instead of the much-derided `stdio`? Absolutely. But `stdio` is so well-supported and easy to implement that it makes complete sense for local servers. Another common complaint is the omission of WebSocket support. Yes, MCP doesn't use WebSocket, but it does use Streamable HTTP - a new protocol that allows for streaming responses over HTTP while maintaining the request-response model. This is a design choice, not an oversight. Using WebSocket brings its own complexities, and getting stuck on the "WebSocket vs HTTP" debate misses the point. MCP is about enabling dynamic, stateful interactions between LLMs and tools, not about picking your favorite transport layer. We're not saying MCP is perfect. The specification is under active development, the tooling is immature, and yes, the initial learning curve is steep. But dismissing it as "over-engineered" often comes from people who haven't grappled with the actual problems it solves. Once the SDKs mature and best practices emerge, much of this complexity will be abstracted away - just like nobody complains about TCP's three-way handshake anymore. ## "The S in MCP is for security" Ah yes, security - MCP's favorite talking point. By now, you've probably seen the breathless blog posts about "Tool Poisoning Attacks" and "Full-Schema Poisoning" complete with scary diagrams and proof-of-concept exploits. **MCP servers are code you run on your machine.** Shocking revelation - if you run malicious code, bad things happen. This isn't a protocol vulnerability, it's basic hygiene. The discovered "vulnerabilities" essentially boil down to: - If you connect to a malicious MCP server, it can trick your LLM into doing bad things. - If an MCP server changes its behavior after you've approved it (a "rug pull"), your LLM might leak sensitive data. - If you paste untrusted data into tool descriptions, the LLM might follow those instructions. In other news, if you install a malicious npm package, it can delete your files. If you `pip install` from a sketchy source, it might mine cryptocurrency. If you `curl | bash` from the internet... well, you get the idea. Yes, MCP has unique attack vectors because LLMs process tool descriptions as instructions. That's concerning and worth addressing. But the "MCP is fundamentally insecure" takes miss the forest for the trees. The real security model is the same as any code execution environment: **Don't run code you don't trust**. The practical mitigations are straightforward: - Review MCP servers before connecting to them (just like you'd review any dependency). - Use signed and versioned MCP servers from trusted sources. - Run MCP servers in containers or sandboxed environments. - Implement proper access controls and least-privilege principles. The MCP ecosystem is already moving in this direction. [Docker's MCP Toolkit](https://docs.docker.com/ai/mcp-catalog-and-toolkit/toolkit/) runs servers in containers. The community is developing security scanners and best practices. The specification now includes security hints like `readOnlyHint` and `destructiveHint`. (I still have my reservations about the annotations, but that's a topic for another day.) Perfect security is the enemy of adoption, and MCP chose pragmatism over paranoia. The security concerns are real, but they're solvable with the same approaches we use everywhere else in software: Trust but verify, defense in depth, and maybe don't give your AI assistant access to your SSH keys without thinking it through first. # When to Use MCP (And When Not To) Source: https://speakeasy.com/mcp/mcp-for-skeptics/when-to-use-mcp MCP's complexity is negligible with the right tools and SDKs. It is only a matter of time before the ecosystem matures enough that complexity becomes a non-issue entirely. But you may need to build tools for agents today, without the luxury of waiting for the ecosystem to catch up. So let's look at some scenarios where MCP's benefits outweigh the complexity _today_. ## MCP is useful when you need dynamic tool discovery If you expect tools to change frequently, or if you want to allow LLMs to discover tools at runtime, MCP's dynamic tool discovery sets it apart from alternatives like function calls. With MCP, the LLM client can query the MCP server for available tools at any time, and the MCP server can dynamically add or remove tools without requiring client changes. With the TypeScript MCP SDK, notifying a client of tool changes is as simple as calling `mcpServer.registerTool()`, `tool.enable()`, or `tool.disable()`. Here's an example of how you might implement dynamic tool discovery in an MCP server: ```typescript import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; const mcpServer = new McpServer({ name: "Example WhatsApp MCP Server", description: "A Model Context Protocol server for WhatsApp", version: "1.0.0", }); const authenticateTool = mcpServer.registerTool("authenticate", { description: "Call this tool to get a QR code to authenticate with WhatsApp", // Tool implementation goes here }); const sendChatTool = mcpServer .registerTool("sendChat", { description: "Call this tool to send a chat message on WhatsApp", // Tool implementation goes here }) .disable(); // Initially disable the sendChat tool whatsAppService.on("authenticated", () => { // When the user authenticates sendChatTool.enable(); // Enable the sendChat tool authenticateTool.disable(); // Disable the authenticate tool // The SDK automatically notifies the MCP client of the tool changes by sending a // `notifications/tools/list_changed` message. // The LLM client can then call `tools/list` to get the updated list of tools // and update the context on the next LLM call. }); mcpServer.connect(new StdioServerTransport()); ``` Implementing dynamic tool discovery with MCP is straightforward. The MCP server can add or remove tools at runtime, and the MCP client can discover these changes without needing to reconnect or reinitialize. Doing the same with function calls would require a custom implementation that tracks available functions and updates the LLM client whenever they change. This is possible, but it adds significant complexity to both the LLM client and custom function codebases. ## MCP is useful when you need stateful interactions Many real-world AI interactions aren't one-shot operations - they're conversations that build on previous context. This is where MCP's stateful architecture shines. Consider a data analysis workflow. An analyst asks their AI assistant to: 1. Connect to the production database 2. Run exploratory queries to understand the schema 3. Identify anomalies in recent transactions 4. Generate visualizations of the findings 5. Create a report with recommendations With function calling, each step is isolated. The LLM would need to keep all the results in its context window. With MCP, the MCP server can maintain state across multiple tool calls, allowing for an asyncchronous, multi-step workflow that builds on previous interactions. The LLM can focus on reasoning and analysis, while the MCP server handles the underlying state management. Here's a concrete example with our WhatsApp MCP server. Imagine implementing a "conversation summary" feature: ```typescript import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; const mcpServer = new McpServer({ name: "WhatsApp MCP Server", description: "A Model Context Protocol server for WhatsApp", version: "1.0.0", }); const getWhatsAppChatById = mcpServer.registerTool("getWhatsAppChatById", { description: "Get a specific WhatsApp chat by its ID", // Define input and output schemas }); const getWhatsAppChatSummary = mcpServer.registerTool( "getWhatsAppChatSummary", { description: "Get a summary of a WhatsApp chat", // Define input and output schemas }, async (args) => { const chat = await getWhatsAppChatById(args.chatId); // Perform analysis on the chat data const analysisId = whatsAppService.queueAnalyzeChat(chat); return { content: [ { type: "text", text: `Chat summary for ${args.chatId}: ${chat.summary}`, }, ], }; }, ); whatsAppService.on("analysisComplete", (analysisId, result) => { // Register the analysis result as a resource mcpServer.resource({ uri: `whatsapp://${analysisId}`, name: `WhatsApp Chat Analysis ${analysisId}`, // implement the resource details to return results }); // The LLM client can then react to this notification, for example by // including a message in the next LLM call: // "New resource available: WhatsApp Chat Analysis 12345 with URL whatsapp://12345" }); mcpServer.connect(new StdioServerTransport()); ``` This statefulness becomes even more powerful with tools like the Playwright MCP server, which maintains a browser session across interactions. The AI can navigate to a page, fill out a form, handle authentication, and scrape results - all while maintaining cookies, session state, and page context. Try doing that with stateless function calls! ## MCP is useful when you need to support multiple LLM APIs The fragmentation in LLM function calling is painfully subtle. As we showed earlier, each provider has its own format, quirks, and limitations. If you're building tools that need to work across multiple LLMs - or if you just want to avoid vendor lock-in - MCP provides a standardized interface. With MCP, you write your tool once as a server, and it works identically whether accessed from: - Claude Desktop - ChatGPT (with the upcoming MCP support) - Open source LLMs via frameworks that support MCP - Your custom application using any LLM provider ## MCP is useful when you need bidirectional communication Traditional function calling is a one-way street: the LLM calls a function and gets a response. But real-world systems often need to push updates to the AI, and this is where MCP's bidirectional communication shines. MCP servers can send notifications to clients about: - New data becoming available - System state changes - Authentication requirements - Progress updates for long-running operations We included an example of this in the previous section, where the WhatsApp MCP server notifies the LLM client when a chat analysis is complete. The LLM client can then react to this notification, for example by including a message in the next LLM call. This bidirectional flow enables truly reactive AI systems that respond to changing conditions, rather than just answering queries. ## MCP comes with a pre-defined authorization model The MCP specification includes OAuth 2.1 support. This allows you to implement secure, standardized authorization flows for your MCP servers. The tooling is ready to use. The TypeScript MCP SDK provides a dead-simple `ProxyOAuthServerProvider` class for servers that need to implement OAuth 2.1 authorization. On the MCP client side, the SDK provides a `OAuthClientProvider` interface that handles the OAuth 2.1 flow for client developers. If you need authorization for function calls, you'll need to implement it yourself. This is often straightforward, but as we all know: Never roll your own authentication. Using the standardized OAuth 2.1 flow provided by MCP is a much safer bet. ## MCP is useful when you need composable, reusable tools One underappreciated benefit of MCP is how it encourages building composable, reusable tools. Because MCP servers are independent processes with standardized interfaces, they naturally become building blocks that different teams can share and combine. We're seeing this play out in the ecosystem: - Teams share MCP servers internally like libraries - Open source MCP servers are proliferating (we've lost count) This composability means you can mix and match capabilities: ```text AI Assistant = Database MCP Server + Slack MCP Server + GitHub MCP Server + Custom Analytics MCP Server ``` Each server is maintained independently, tested separately, and can be updated without affecting others. This modular approach is much cleaner than cramming all functionality into a monolithic set of function calls. # Monitor your MCP server Source: https://speakeasy.com/mcp/monitoring-mcp-servers MCP servers connect LLM agents to external services like databases, APIs, and internal tools. When something breaks, you need to know where the problem has occurred: Is your MCP server failing? Is the LLM making bad tool calls? Is there latency from your backend services or LLM processing? API and web server monitoring involves tracking HTTP response times, error rates, HTTP call patterns, and other key metrics. Monitoring MCP setups requires more. The LLM interprets user intent and decides which tools to call. You need to monitor both the MCP server's performance and the LLM's tool usage patterns. If you don't monitor your MCP server: - **Tool call patterns stay hidden:** You can't optimize which tools to improve or remove. - **Failures go undiagnosed:** Tool calls fail silently, and you don't know if bad parameters, backend issues, or LLM errors are at fault. - **Latency problems compound:** Slow tools delay every agent response, but you need per-tool latency metrics to identify the specific tool causing the bottleneck. - **Security breaches go undetected:** You risk overlooking unusual access patterns indicating prompt injection or data exfiltration. This guide covers the metrics you should monitor and demonstrates how to set up monitoring for both self-hosted and distributed MCP servers. ## What to monitor in your MCP server Your MCP server acts as the bridge between LLM agents and your backend services. The LLM requests tools, resources, or prompts from your MCP server, and your MCP server executes those requests. ![MCP server](/assets/mcp/monitoring-your-mcp-server/mcp-architecture.png) Your monitoring should focus on the MCP server layer, collecting information about the tools that agents access, how the tools perform, and whether their usage patterns appear normal or suspicious. ### Tool call metadata Every time an agent calls a tool, you need structured data about that invocation. This creates an audit trail showing the agent's actions, including when they were performed and whether they were successful. Capture the following metadata: - **Tool names and timestamps** to identify which tools were called and when. This allows you to correlate tool calls with coinciding user complaints or backend issues. - **Input parameters (sanitized if sensitive)** to see what data the agent passed to the tool. When a tool fails, verify whether the agent sent malformed input, missing required fields, or invalid values. - **Success or failure statuses** to track failure rates per tool, so you can identify unreliable tools that need optimization or better error handling. - **Client or session identifiers** to link tool calls to specific users or sessions. Ideally, when a user reports an issue, you can trace their entire interaction history across multiple tool calls. - **Error messages when tools fail** to gain insight into why the failure happened. Was it due to backend timeout, a database connection error, or an invalid API key? Error messages indicate where to begin debugging. ### Tool usage patterns Beyond individual tool calls, you need to understand the bigger picture: which tools get used most, which ones fail together, and whether usage patterns change suddenly. Capture the following usage patterns: - **Tool call frequency per client** to establish a baseline of normal behavior. If a client that typically makes five calls per hour suddenly makes 60 calls, that signals a problem. The increase in calls could be caused by a runaway LLM agent or potentially by abuse. - **Tool call sequences** to identify which tools are commonly used in workflows. When agents consistently chain Tool A with Tool B, the recurring pattern reveals an optimization opportunity. Learn more in the guide to [optimizing your MCP server](/mcp/tool-design). - **Sudden spikes in specific tool usage** to catch anomalies. A tenfold increase in database query tools might indicate a prompt injection attack attempting data exfiltration or an agent stuck in a loop making the same call repeatedly. For security-focused monitoring, such as authentication failures and unauthorized access attempts, read our guide to [securing your MCP server](/mcp/securing-mcp-servers). ### Tool performance metrics Some tools respond within milliseconds; others take seconds. Some tools return compact JSON; others dump massive payloads. These differences matter because slow or bloated tools block the entire agent workflow. Track the following performance metrics: - **The latency per tool** to identify which tools slow down your entire system. Monitor the 50th, 95th, and 99th percentiles. For example, if a tool shows a 100-millisecond average latency but a five-second latency at the 99th percentile, you know that some requests block the agent for five seconds, degrading the user experience. - **The response payload size per tool** to catch the tools returning excessive data. A tool that dumps 10KB when 1KB would suffice wastes tokens and clogs the LLM's context window. Large payloads also increase the network transfer time and processing overhead. ### Server-level metrics Your MCP server's overall health determines whether it can handle the load that agents throw at it. High error rates, resource exhaustion, and rate limiting all degrade performance before they cause complete outages. Monitoring server-level metrics allows you to catch these capacity problems early, giving you time to scale or optimize resources before users experience failures. Track the following server-level metrics: - **The overall error rate across all tools** to detect infrastructure issues. For example, if error rates spiked across multiple tools simultaneously, the problem would likely be in shared infrastructure (such as database connections, authentication services, or network issues) rather than individual tool logic. - **Request volumes and rate-limit hits** to understand if you need to scale capacity or if specific agents are making excessive calls. The number of rate limits hit indicates whether your infrastructure can't keep up with demand or a single client requires throttling. - **Memory and CPU usage during high tool call volume** to identify resource constraints before they cause crashes. High memory usage during peak hours signals that you should add resources or optimize tool implementations that are leaking memory or consuming excessive CPU. ## How to monitor your MCP server Your monitoring approach will vary based on how you deploy your MCP server. Remote servers offer you complete control over your infrastructure and tooling, whereas packaged servers require opt-in telemetry that respects user privacy. ### Remote servers When you host your MCP server on your own infrastructure, you control the entire monitoring stack and can use standard production tooling. #### Logging Logging captures your tools' performance in real time. When a tool fails, logs provide information about the parameters passed, the external APIs called, and the location of the failure. You can use FastMCP for logging as follows: ```python import logging from fastmcp import FastMCP, Context # Server-side logging logger = logging.getLogger(__name__) mcp = FastMCP("MCPServer") @mcp.tool async def search_data(query: str, ctx: Context): logger.info("Tool called: search_data", extra={"query": query}) try: results = perform_search(query) logger.info("Search completed", extra={"result_count": len(results)}) return results except Exception as e: logger.error("Search failed", exc_info=True) raise ``` You can then send these logs to Elasticsearch, CloudWatch, Datadog, or your preferred aggregation platform. Using structured logging (using the `extra` parameter) enables you to search and filter your logs. #### Server-level monitoring with Sentry Server-level monitoring tracks your MCP server's overall health, recording error rates, uptime, and resource usage. Sentry captures errors with full context, showing you what was happening when failures occurred. You can instrument MCP tool calls for distributed tracing using `@sentry/node`: ```ts import * as Sentry from "@sentry/node"; const registerTool = (server, name, schema, handler) => { server.tool(name, schema, async (args) => { return await Sentry.startNewTrace(async () => { return await Sentry.startSpan( { name: `mcp.tool/${name}`, attributes: { "tool.name": name, // Avoid logging raw tool parameters; sanitize or omit sensitive input "tool.params_present": args != null, }, }, async (span) => { try { const result = await handler(args); span.setAttribute("tool.status", "success"); return result; } catch (err) { span.setAttribute("tool.status", "error"); Sentry.captureException(err); throw err; } }, ); }); }); }; registerTool( server, "search_data", { query: z.string() }, async ({ query }) => { // Your tool implementation const results = await searchDatabase(query); return results; }, ); ``` This manual instrumentation captures: - Tool call latency from invocation to completion - Input parameters passed to each tool - Success or failure statuses - Full error stack traces when tools fail - Distributed traces showing downstream API calls Each tool invocation creates a trace you can inspect in Sentry's dashboard. When a tool fails, Sentry shows the exact parameters that caused the failure, the backend API calls that were made, and where the error originated. For complete implementation, including error handling, user context binding, and Cloudflare Workers setup, see the Sentry guide to [monitoring MCP servers](https://blog.sentry.io/monitoring-mcp-server-sentry/). ### Packaged servers (distributed) When you distribute your MCP server as an npm package, as a GitHub repository for cloning, or as an MCPB file, users run it on their own machines. You have no access to their infrastructure, so monitoring requires opt-in telemetry. #### OpenTelemetry for distributed servers OpenTelemetry provides vendor-neutral telemetry collection. Users must explicitly enable it through environment variables. ```python import os from opentelemetry import trace from opentelemetry.sdk.trace import TracerProvider from opentelemetry.sdk.trace.export import BatchSpanProcessor from opentelemetry.exporter.otlp.proto.grpc.trace_exporter import OTLPSpanExporter # MONITOR: Check if user enabled telemetry (disabled by default) TELEMETRY_ENABLED = os.getenv("MCP_TELEMETRY_ENABLED", "false").lower() == "true" TELEMETRY_ENDPOINT = os.getenv("MCP_TELEMETRY_ENDPOINT", "https://your-telemetry-endpoint") if TELEMETRY_ENABLED: # MONITOR: Initialize OpenTelemetry only when user opts in provider = TracerProvider() processor = BatchSpanProcessor( OTLPSpanExporter(endpoint=TELEMETRY_ENDPOINT) ) provider.add_span_processor(processor) trace.set_tracer_provider(provider) tracer = trace.get_tracer(__name__) def search_data(query: str): if TELEMETRY_ENABLED: # MONITOR: Track tool execution when telemetry enabled with tracer.start_as_current_span("mcp.tool/search_data") as span: span.set_attribute("tool.name", "search_data") # PRIVACY: Never log user input span.set_attribute("tool.invoked", True) try: results = perform_search(query) # MONITOR: Track success metrics only span.set_attribute("tool.status", "success") span.set_attribute("result.count", len(results)) return results except Exception as e: # MONITOR: Track error type, not error details span.set_attribute("tool.status", "error") span.set_attribute("error.type", type(e).__name__) span.record_exception(e) raise else: # No telemetry, execute normally return perform_search(query) ``` Include a README file informing users what data you collect and how they can control it in the configuration. Telemetry should be disabled by default: ```bash MCP_TELEMETRY_ENABLED=false ``` For a complete OpenTelemetry implementation with multiple language examples, see SigNoz's guide to [MCP observability](https://signoz.io/blog/mcp-observability-with-otel/). ## What not to monitor Although MCP is a new technology, it's built on software and APIs that are subject to compliance, security, and privacy regulations. Avoid collecting user output data, authentication data and credentials, personally identifiable information (PII), and business-sensitive information. User output data includes: - Tool response content, such as search results, API responses, and generated content - Conversation history and session data Authentication data and credentials include: - API keys, access tokens, and passwords - OAuth tokens and session identifiers - Database connection strings and credentials PII includes: - Email addresses, phone numbers, and physical addresses - Names, usernames, and user IDs that can be used to identify individuals - IP addresses and device identifiers - Any data subject to GDPR, CCPA, or similar privacy regulations Business-sensitive information includes: - Customer data and business records accessed through tools - Internal system paths, database schemas, and infrastructure details - Third-party API responses that may contain proprietary data When in doubt, don't collect it. If you need to debug issues, use correlation IDs or session identifiers that can only be traced back to individuals through the use of additional context that you control. ## Final thoughts Ensure the success of your MCP server by monitoring it to learn when tools fail, which tools are slow, and how users interact with your server. Start with error tracking and basic logging, then add distributed tracing when you need to debug complex failures. For self-hosted servers, use standard monitoring platforms. For distributed servers, implement opt-in telemetry and avoid collecting user data. Use monitoring data to optimize your MCP server's performance and strengthen its security. See our guides to [optimizing](/mcp/tool-design) and [securing](/mcp/securing-mcp-servers) your MCP server. # The OpenAI ecosystem: A developer's guide to building agents with OpenAI Source: https://speakeasy.com/mcp/openai-ecosystem import Image from "next/image"; OpenAI ships something new almost every month. Models get renamed, preview features appear, and entire product lines arrive with little warning. If you're building agents, you need a clear picture of which pieces expect code, which are visual builders, and how they all fit together. This guide walks developers through OpenAI's ecosystem with a focus on building agents. We compare code-first APIs with no-code builders, explain what each product does, and where relevant, demonstrate how to integrate them with external tools using MCP. ## Code or no-code OpenAI offers two main paths for building agents: - The code-first route gives developers full control over prompts, state, and infrastructure. - The no-code route provides visual builders and managed hosting that business users can operate. You'll likely start with one and migrate to the other, or run both in parallel. Here's how they compare: | | Code-first APIs | No-code builders | |------------------|-------------------------------------------------------------|--------------------------------------------------------------| | Control level | Total control over prompts, state, infrastructure | Configuration only, OpenAI hosts | | Who maintains it | Developers | Product managers, support staff, operations, also developers | | Tool integration | Call any external service via code | Use built-in connectors or workspace-approved integrations | | When to use | Custom apps, compliance-driven workloads, advanced copilots | Fast pilots, embedded assistants, business-run automations | | Key products | Responses API, Realtime API, Agents SDK | AgentKit, ChatGPT GPT Builder, Workflows, Operator | The rest of this guide digs into each path and explains how the products within them work. ## Building agents with code This is the best place to start if you want full control over the prompts, latency, and infrastructure of your agent. The following APIs all sit behind the same pay-as-you-go account. ### Responses API: The foundation The Responses API replaces the old Chat Completions endpoint and the legacy Assistants API. It accepts multimodal input, streams outputs, handles tool calling and JSON mode, and supports the newer reasoning models (o1 and o3). Compared to the legacy Assistants API, the Responses API is stateless by default, so you decide where conversation history lives. Similarly, it improves on the old Chat Completions API by adding first-class JSON Schema validation and tool definitions. Use the Responses API when you need a single request-response cycle: send a prompt, and get a completion. The API supports function calling, so your agent can invoke tools mid-generation. You define tools using the JSON Schema, the model decides when to call them, and you execute the tool logic in your code before sending the result back. The following basic example calls the Responses API directly: ```ts import OpenAI from "openai"; const openai = new OpenAI({ apiKey: process.env.OPENAI_API_KEY }); async function main() { const response = await openai.responses.create({ model: "gpt-5", input: "What's the weather in San Francisco?", tools: [ { name: "get_weather", type: "function", function: { description: "Get current weather for a location", parameters: { type: "object", properties: { location: { type: "string" }, }, required: ["location"], }, }, }, ], }); console.log(response.output_text); } main(); ``` ### Realtime API: Voice and streaming The Realtime API handles WebRTC or WebSocket sessions with sub-second latency. You stream audio, tool results, or screen events, and the model responds in the same channel. Compared to plain Responses API calls, you trade stateless requests for a session object that handles turn-taking. Use the Realtime API for voice conversations, live transcription, or any scenario that requires continuous bidirectional communication. The API manages the session state, handles interruptions (when a user speaks over the model), and supports tool calling throughout the conversation. The diagram below shows the Realtime API architecture: - Your application establishes a WebSocket or WebRTC connection. - Audio streams in both directions, and the model can invoke tools mid-conversation. - When a tool call happens, the session pauses, your code executes the tool, and the result streams back into the conversation. This architecture enables natural voice interactions. The model can ask clarifying questions, call tools to fetch data, and respond with synthesized speech in real time without breaking the conversation flow. Check out our [MCP use case guide for Realtime agents](/mcp/using-mcp/use-cases) to learn how to build a voice assistant with the Realtime API. ### Agents SDK: Orchestration in code The Agents SDK is for developers who want to define agent logic in code rather than configuration. It works with both the Responses API and the Realtime API, allowing you to programmatically define workflows in Python (with Node.js support coming). The SDK is open source, so you can version control your agent definitions and test them locally before deployment. **How it works:** You define agents with instructions, tool catalogs, and models in code. The Agents SDK handles multi-step workflows, persistent state, and evaluation traces. Unlike the stateless Responses API, the SDK manages conversation history, tool outputs, and run metadata for you. Here's a basic example: ```python import asyncio from typing import Annotated from pydantic import BaseModel, Field from agents import Agent, Runner, function_tool class Weather(BaseModel): city: str = Field(description="The city name") temperature_range: str = Field(description="The temperature range in Celsius") conditions: str = Field(description="The weather conditions") @function_tool def get_weather(city: Annotated[str, "The city to get the weather for"]) -> Weather: """Get the current weather information for a specified city.""" print("[debug] get_weather called") return Weather(city=city, temperature_range="14-20C", conditions="Sunny with wind.") agent = Agent( name="Hello world", instructions="You are a helpful agent.", tools=[get_weather], ) async def main(): result = await Runner.run(agent, input="What's the weather in Tokyo?") print(result.final_output) # The weather in Tokyo is sunny. if __name__ == "__main__": asyncio.run(main()) ``` - **Run steps and tool calls:** The SDK exposes run steps, which show you exactly what the agent did during execution — which tools it called, which parameters it used, and what the tools returned. This is critical for debugging and evaluation. You can inspect failed runs to see which tool timed out or which LLM call hallucinated. Run steps also let you forward tool calls to MCP servers. When the agent decides to call a tool, you read the tool name and parameters, call your MCP server (via stdio or HTTP), and submit the tool output back. The agent resumes and continues the workflow. - **Multi-model runs:** The Agents SDK can switch models mid-run. For example, it may start with GPT-5-mini for simple questions, but if the agent determines it needs deeper reasoning, it escalates to GPT-5. This saves costs on routine queries while maintaining quality for complex cases. - **Evaluation traces:** The SDK logs every decision the agent makes — which tools it considered, which it rejected, what prompts it sent to the model, and what responses it received. These traces help you benchmark agent performance, spot regressions, and fine-tune instructions. - **MCP integration:** The Agents SDK uses the same tool schema as MCP, so you can define your tool catalog once and reference it from both your SDK agents and your MCP servers. When an agent needs to call a tool, you can delegate to an MCP server via stdio or HTTP, keeping your agent logic portable across runtimes. > **Note:** OpenAI also offers the Assistants API, a REST API for building assistants with threads and runs. The Assistants API is being deprecated in 2026 in favor of the Responses API, which will incorporate all assistant features. For new projects, use the Agents SDK for code-based workflows or the Responses API for stateless interactions. ### Codex: AI for software development Codex is OpenAI's specialized agent for coding tasks. It runs the GPT-5-Codex family of models and integrates directly into your development workflow. Codex handles code generation, editing, review, and infrastructure automation. It's included with the ChatGPT Plus, Team, and Enterprise subscriptions. You can install the **Codex CLI** globally with the command: ```bash npm i -g @openai/codex ``` When you run the Codex CLI in your terminal, it watches your repository, responds to natural language prompts, and executes file edits, Git operations, and shell commands. It maintains context across your codebase, so you can ask it to refactor a function and it will find all the call sites. The Codex CLI logs all operations and asks for confirmation before destructive actions (such as deleting files or force-pushing to Git). You can integrate it into CI/CD pipelines or use it for one-off migrations. You can also run it inside Docker containers for sandboxed code generation. Codex maintains context across multiple exchanges: It reads files, proposes changes, and asks for approval before applying edits. The colored terminal output distinguishes between analysis (understanding your request), planning (what it intends to do), and actual file operations. Codex references specific files and line numbers when explaining changes, making it easy to spot when it misunderstands your intent. Codex also includes a **web UI and IDE extensions**. The web dashboard mirrors the CLI but adds a visual diff viewer and approval workflow. The interface splits into two panels, with conversation on the left and side-by-side diffs on the right. You can approve all changes at once or review each file individually. Shell commands (such as tests, builds, and Git operations) require explicit approval before execution. This visual approval workflow works well for code reviews. A reviewer can ask Codex to implement feedback, inspect the proposed changes, and approve or request modifications without switching to a terminal. The Codex **Slack and GitHub integrations** allow it to respond to coding questions in Slack channels, generate pull requests from feature requests, or review PRs when tagged. When hooked into PR workflows in GitHub, Codex comments on PRs with suggestions, runs static analysis, and autogenerates changelog entries based on commits. Similarly, the **MCP integration for Codex** lets it use MCP internally to call external tools. When you register MCP servers in your Codex config (CLI or IDE), Codex can invoke them when it needs domain-specific functionality. For example, if you [register an MCP server that queries docs](https://context7.com), Codex can look up endpoint schemas before generating integration code. ## Building agents without code Reach for these tools when you want distribution, guardrails, or quick experiments. The following no-code products are ideal for non-technical users who want to launch agents without touching source code. ### AgentKit: Visual agent building AgentKit launched at OpenAI DevDay 2025 as a complete toolkit for building, deploying, and optimizing agents without writing orchestration code. It includes four main components. #### Agent Builder Agent Builder is a visual canvas for building multi-agent workflows using drag-and-drop nodes. Think of it as "Canva for agents" — you drag nodes onto a canvas and connect them with arrows to define logic. Agent Builder includes the following node types: - **Agent nodes** are LLM processing steps that reason over inputs and generate outputs. - **Logic nodes** add conditionals (if/else), loops, and branching to a workflow. - **Tool connectors** add MCP servers, API calls, and database queries. - **User approval nodes** pause workflows for human review. - **Guardrail nodes** enforce safety constraints on outputs. - **Data transformation nodes** format, filter, or restructure data between workflow steps. The Agent Builder canvas includes versioning, preview runs, and templates for common patterns (such as customer service, data enrichment, and document comparison). When you build a workflow, you test it live in a preview panel that shows which nodes execute and which branches the agent takes. Agent Builder compiles workflows to Agents SDK runs, so you can copy your workflows as code that is fully compatible with the Agents SDK. You can also connect MCP servers as nodes to extend your workflows with your own custom logic and data. #### ChatKit ChatKit is an embeddable chat interface that brings AI assistants into web and mobile apps. It provides a production-ready chat UI with built-in tool integration, conversation state, and streaming responses. Embed ChatKit into your app using the JavaScript library (React, Vue, Angular, and vanilla JS are all supported): ```jsx import { ChatKit } from "@openai/chatkit-react"; function App() { return ( ); } ``` ChatKit handles user identity automatically. Just pass a `userId`, and the assistant remembers context across sessions. The widget includes typing indicators, file uploads (PDFs, images), voice input, and conversation history. Users can scroll through past conversations or start new threads. #### ChatKit Studio ChatKit Studio is a visual interface for building and configuring ChatKit instances without code. It lets you configure models, instructions, data sources, tools, and UI themes, with a live preview panel for testing the assistant before deployment. We recommend using an OpenAI-hosted backend for ChatKit, where OpenAI handles hosting and scaling, but a self-hosted option is also available if you want to run ChatKit on your own infrastructure using the ChatKit Python SDK. ### ChatGPT MCP connectors ChatGPT supports MCP connectors across the Plus, Pro, Team, Enterprise, and Edu tiers. Register your MCP servers through the Connectors interface, and ChatGPT can call them during conversations. Unlike earlier limitations, MCP connectors now support both read and write operations, so ChatGPT can update tickets in your issue tracker, trigger workflows in your automation system, write to databases, and call internal APIs. MCP servers connect to ChatGPT via remote endpoints using Server-Sent Events (SSE) or streaming HTTP protocols. OAuth handles authentication when needed. ChatGPT displays confirmation modals before executing write or modify actions, and workspace admins control which users can register custom connectors. #### Atlas Browser OpenAI launched ChatGPT Atlas in October 2025, a Chromium-based browser with ChatGPT built into the browsing experience. Agent mode (available for Plus, Pro, and Business users) lets ChatGPT work autonomously within the browser, researching topics, automating tasks, planning events, or booking appointments while you browse. Atlas also has first-class support for ChatGPT connectors as well as for Apps SDK-based integrations. ### Widget Builder and Apps SDK: From limited tools to full dev support ChatGPT's development story started with `/search` and `/fetch` as the only tools available for deep research. Today, these limited tools have evolved into full developer support through the Apps SDK and Widget Builder. #### Apps SDK The Apps SDK is the foundation for building ChatGPT integrations. It bridges your backend services and ChatGPT, letting you define tools, handle authentication, and manage state across conversations. The SDK handles the protocol details so you focus on exposing your API's capabilities. At its core, the Apps SDK is an MCP server that runs in your infrastructure. You define tools using the standard MCP schema (with tool names, descriptions, input schemas, and handler functions). ChatGPT discovers these tools and calls them when users ask for something your app can do. The SDK adds features beyond basic MCP, including OAuth flows for user authentication, session persistence so your tools remember context across messages, and rate limiting per user or per workspace. It handles error recovery automatically. If your tool times out or returns an error, the SDK retries or prompts the user for clarification. Here's how you define a tool with the Apps SDK: ```ts async function loadKanbanBoard() { const tasks = [ { id: "task-1", title: "Design empty states", assignee: "Ada", status: "todo" }, { id: "task-2", title: "Wireframe admin panel", assignee: "Grace", status: "in-progress" }, { id: "task-3", title: "QA onboarding flow", assignee: "Lin", status: "done" } ]; return { columns: [ { id: "todo", title: "To do", tasks: tasks.filter((task) => task.status === "todo") }, { id: "in-progress", title: "In progress", tasks: tasks.filter((task) => task.status === "in-progress") }, { id: "done", title: "Done", tasks: tasks.filter((task) => task.status === "done") } ], tasksById: Object.fromEntries(tasks.map((task) => [task.id, task])), lastSyncedAt: new Date().toISOString() }; } server.registerTool( "kanban-board", { title: "Show Kanban Board", _meta: { "openai/outputTemplate": "ui://widget/kanban-board.html", "openai/toolInvocation/invoking": "Displaying the board", "openai/toolInvocation/invoked": "Displayed the board" }, inputSchema: { tasks: z.string() } }, async () => { const board = await loadKanbanBoard(); return { structuredContent: { columns: board.columns.map((column) => ({ id: column.id, title: column.title, tasks: column.tasks.slice(0, 5) // keep payload concise for the model })) }, content: [{ type: "text", text: "Here's your latest board. Drag cards in the component to update status." }], _meta: { tasksById: board.tasksById, // full task map for the component only lastSyncedAt: board.lastSyncedAt } }; } ); ``` The `context` object gives you access to the authenticated user, their workspace, and any state you've stored in previous tool calls. This lets you build stateful agents that remember preferences, cache API responses, or track multi-step workflows. The Apps SDK supports multiple authentication patterns. - **For workspace integrations:** Use OAuth to let users connect their accounts (for example, Slack, Google Calendar, or Salesforce). The SDK stores the OAuth tokens securely and refreshes them automatically. Your tool handlers receive the user's token via the context, so you make API calls on their behalf without exposing credentials to ChatGPT. - **For internal tools:** Skip OAuth and rely on ChatGPT's workspace auth. If an admin registers your app in a Team or Enterprise workspace, the SDK trusts that all users in that workspace are authorized. #### Widget Builder Built on top of the Apps SDK, Widget Builder lets you return rich UI components from your MCP tools, not just plain text. When ChatGPT calls your tool, it responds with interactive widgets, such as data tables, forms, charts, or custom visualizations. The user sees these rendered directly in the ChatGPT interface. Widgets bind to data using placeholders like `{{task.title}}` that fill in when your tool returns results. You can rapidly develop widgets using the low-code Widget Builder in ChatKit Studio. With Widgets, your MCP server does more than answer questions — it becomes interactive. A task management tool returns a widget with checkboxes to complete tasks. A CRM tool returns a customer card with click-to-call buttons. A metrics tool returns live charts that update when the user asks follow-up questions. Your MCP server defines tools with a `_meta` field that points to a widget URI. When ChatGPT calls the tool, your server returns data and the widget template. ChatGPT renders the widget using your template and the returned data. ```ts server.registerResource( "html", "ui://widget/widget.html", {}, async (req) => ({ contents: [ { uri: "ui://widget/widget.html", mimeType: "text/html", text: `
`.trim(), _meta: { "openai/widgetCSP": { connect_domains: [], resource_domains: ["https://persistent.oaistatic.com"], } }, }, ], }) ); ``` ### Workflows and Operator Workflows let you pin deterministic steps around model calls. You define stages that can branch, await human review, or trigger tools. Operator is OpenAI's research preview agent that controls a browser and handles long-running tasks. Both use the same tool definition format as the Responses API. Register your MCP tools once and make them available across these runtimes. Workflows is currently invite-only for enterprise customers; Operator is also on a waitlist and expects clear guardrails in your application. ## The supporting cast: Models, creative tools, and infrastructure These services share billing with the APIs but introduce their own review processes and constraints. We'll cover them briefly so you know where they fit. ### Models: Naming and routing OpenAI's model names shift frequently. As of early 2025, the main families are: - **GPT-5:** The flagship model for complex reasoning and multi-step tasks - **GPT-5-mini:** A lightweight variant optimized for speed and cost that is good for high-volume tasks - **GPT-5-nano:** The smallest, cheapest model in the family, which is best for fast responses for simple queries - **GPT-5-Codex (high), GPT-5-Codex (medium), GPT-5-Codex (low):** Specialized models for code generation and software development tasks, where high offers the most accuracy and low prioritizes speed All these models are available through the Responses API and ChatGPT (depending on your tier). The Codex variants power the Codex developer tools. Pricing varies by model, so we recommend routing expensive models through a dedicated MCP tool to audit usage. ### Creative endpoints: Images, audio, and video **DALL-E 3 image generation:** DALL-E 3 handles prompt-to-image generation, inpainting, and image variation. The responses return URLs or Base64 blobs. When using MCP, you wrap the call in a tool and return a resource with the download link so the client can show a thumbnail or fetch the file later. **Voice Engine and the Audio API endpoint:** With these tools, OpenAI covers both text-to-speech and speech-to-text. When these capabilities are paired with the Realtime API, latency is low enough for real-time agents. A common pattern is an MCP tool that takes text, calls the audio API, uploads the result to object storage, and returns a resource URI for the client to stream. This is the same pattern OpenAI uses in ChatGPT's mobile apps when you ask about a photo and expect a spoken answer. **Sora video:** OpenAI's text-to-video model, Sora, generates short, high-fidelity clips. Access is currently limited to creative partners and enterprise pilots. Rendering jobs take minutes, so treat them as asynchronous tasks: expose an MCP tool that submits the prompt, return a completion handle, and stream progress updates as the job advances. ### Infrastructure and retrieval **Embeddings and vector stores:** Embeddings endpoints (`text-embedding-3-large`, `text-embedding-3-small`, and domain variants) power retrieval pipelines. Vector stores add metadata filtering and chunk management. These services are regional, so double-check availability if your MCP servers run in another region. Treat long-running jobs as MCP completables: queue the work, return a completion handle, and let the client poll or subscribe. **Fine-tuning:** The Fine-tuning endpoint adapts a base model to your tone or domain, but you still call the resulting model through the Responses API. It lets you designate jobs and use fine-tuned model IDs in subsequent calls. **Batch jobs:** Batch processing lets you send tens of thousands of prompts at a lower cost. It pairs nicely with MCP. Expose a `submit_batch` tool and emit status updates as resources. **Files API:** The Files endpoint stores training data or retrieval corpora and is the only sanctioned way to share larger documents with OpenAI services. ## Connecting it all: MCP integration patterns MCP gives you one adapter surface as OpenAI's products evolve. Here are the main patterns for integrating your MCP servers with OpenAI's ecosystem. ### Direct integration: MCP server wraps OpenAI API This is the pattern we showed earlier with the Responses API. Your MCP server keeps your OpenAI API key server-side and exposes a tool that calls the API. The client (Claude, Cursor, or another MCP host) calls your tool, and you forward the request to OpenAI. This keeps secrets off the client and lets you audit all OpenAI usage in one place. Add rate limiting, cost tracking, or custom logging before forwarding calls. ### Reusable catalogs: Call from multiple runtimes The biggest MCP benefit is consistency. Once you model an internal API as an MCP server, you call it from Claude, ChatGPT, Cursor, and any other MCP-compatible runtime. Define your tool schema once, register it in ChatGPT Team workspaces and Claude projects, and both runtimes use it. When OpenAI adds a new agent product (like Workflows or Operator), you don't rebuild integrations — you just point it at your existing MCP catalog. ### Web search: Built-in tools and Deep Research **Responses API web search:** The Responses API includes a `web_search` tool that you enable per account. Pass the tool in your request, and the model decides when to use it. When triggered, the API returns responses with URL citations and source annotations. The limitation is that you don't control the search provider, citation format, or result ranking. **Deep Research:** ChatGPT's Deep Research feature (launched February 2025) is an autonomous agent that conducts multi-step web research. Give it a prompt, and it searches, analyzes, and synthesizes hundreds of sources to produce a comprehensive report. Deep Research takes 5-30 minutes and uses a version of the o4 model optimized for web browsing. It's available in the ChatGPT Plus, Team, Enterprise, and Pro tiers, with API access launched in June 2025. **Custom retrieval with MCP:** Instead of conducting generic web searches, Deep Research supports MCP tool queries that can fetch internal documentation, compliance databases, or proprietary data sources. This returns structured results (document titles, relevance scores, snippets, and metadata) that the model can reason over. Custom retrieval gives you control and auditability — you can log every query and view citations you can verify. ## Plans, pricing, and requirements OpenAI's offerings cluster into several pricing tiers. Here's a snapshot as of early 2025 to help you navigate what you need. ### API pay-as-you-go Pay-as-you-go is required for Responses API, Realtime API, Assistants API, embeddings, vector stores, fine-tuning, batch jobs, and creative endpoints. You pay per token or per request, and the price varies by model. Most regions require a verified billing method and a $5 prepayment to activate the account. Reasoning models (`o1` and `o3`) cost significantly more per token, so we recommend routing them through a dedicated MCP tool to audit usage. ### Add-on tools Adding web search capability enables `web_search` and `url_get` tools in the Responses API. Web search is still rolling out; Plus, Team, and Enterprise customers can usually flip it on through the dashboard, while some organizations need to email support to request access. Pricing is per search query (roughly $10 per 1,000 searches). To avoid this cost, build your own search MCP tool instead. ## Next steps for builders If you're a developer, start by building MCP servers that expose your own tools and data sources. Create servers that query your databases, call your internal APIs, access your file systems, or integrate with services you use (like GitHub, Stripe, and CRMs). Define each tool using the JSON Schema, handle authentication, and test locally before deploying. This gives you a reusable catalog of tools that works across Claude, Cursor, ChatGPT, and any other MCP-compatible runtime. If you're building a platform, decide whether to centralize tools using the Agents SDK or expose them directly through ChatGPT workspaces with the Apps SDK. A common approach is to use both: maintain one MCP catalog that powers both SDK-driven agents and workspace GPTs. For ChatGPT Team or Enterprise plans, register your MCP servers in the workspace so every GPT model inherits them automatically. This means you approve tools once, and they become available everywhere, including in ChatKit embeds and the desktop apps. Keep an eye on OpenAI's release notes. This ecosystem shifts fast, but MCP gives you one adapter surface as the products evolve. The more you express your systems as MCP tools, the easier it is to evaluate whatever OpenAI launches next without rebuilding integrations. # What is MCP? Source: https://speakeasy.com/mcp/overview import Image from "next/image"; The Model Context Protocol (MCP) is an **open standard that enables AI agents to securely connect to external tools, data sources, and services**. Think of it as a universal translator between AI models and the real world. ## The problem MCP solves Before MCP, connecting AI agents to external systems required building custom integrations for each combination of agent and tool. This meant: - **M × N integration complexity**: Every AI agent needed custom code for every tool it wanted to use. - **A fragmented ecosystem**: There was no standardized way for tools to expose their capabilities to AI agents. - **Security challenges**: Each integration handled permissions and access differently. - **More maintenance overhead**: Updates to any system broke multiple custom integrations. In the diagram above, we see how each application (Claude Desktop, VS Code, Cursor) requires custom integrations to access various tools (GitHub, PostgreSQL, the user's file system). This creates a complex web of M × N connections that is difficult to maintain and scale. With MCP, we introduce a standardized protocol that simplifies this process. There is now a single integration point for each tool, allowing any compatible AI agent to access it without custom code. In the diagram above, we see how MCP simplifies the architecture. Each AI agent (Claude Desktop, VS Code, Cursor) connects through a single MCP layer to access multiple tools (GitHub, PostgreSQL, the user's file system). ## How MCP works MCP establishes a client-server architecture made up of: - **MCP clients**: AI agents, applications, or development tools (like Claude Desktop, VS Code extensions, and Cursor) - **MCP servers**: Services that expose tools, data, or capabilities (like GitHub, databases, and APIs) - **A standardized protocol**: A common language both clients and servers understand Instead of building custom bridges, you now have: - **M + N simplicity**: One MCP integration per tool, which works with all compatible AI agents - **Standardized security**: Consistent authentication and authorization patterns - **Easy maintenance**: Protocol updates that benefit the entire ecosystem ## Key benefits of MCP Depending on your role in the ecosystem, MCP provides several advantages. ### Advantages for AI agent users When using MCP-compatible AI agents, like Claude Desktop, VS Code extensions, or Cursor, you get: - Access to more tools and data sources without custom integrations - A consistent experience interaction pattern across different AI agents - Better security through standardized authentication and authorization ### Advantages for tool providers When you build an MCP server to expose your tools, you gain: - A single integration point for all compatible AI agents - More users for your tools without custom integration work - Capacity to focus on your core product instead of on AI integrations ### Advantages for agent developers When you build MCP-compatible applications or agents, you benefit from: - Access to a growing ecosystem of tools and services - Simplified development with standardized protocols - Increased collaboration opportunities across different AI agents ## Real-world example of MCP in action See how MCP simplifies the process of connecting AI agents to external tools and data sources. Imagine you want to use Claude Desktop to read a GitHub issue from your company's private repository and query your PostgreSQL database for related data. **The manual process** If this were a once-off task, you would typically need to: 1. Copy and paste the contents of the GitHub issue into Claude Desktop. 2. Describe the database schema to Claude. 3. Ask Claude to write a custom query based on the pasted schema. 4. Copy and paste the query into your database client. 5. Copy and paste the results back into Claude Desktop. 6. Manually combine the information from the GitHub issue and database query results. **The custom integration process** If you did this often, you might write a custom integration that connects Claude Desktop to GitHub and PostgreSQL, which would require ongoing maintenance and updates. The steps involved are cumbersome and error-prone, especially if the GitHub issue or database schema changes. This would get even more complex if Claude needed to wait for long-running queries or if you wanted to access multiple tools at once. **The MCP way** With MCP, you would: 1. Install the [GitHub MCP Server](https://github.com/github/github-mcp-server) and a [PostgreSQL MCP server](https://www.npmjs.com/package/@modelcontextprotocol/server-postgres). 2. Ask Claude: _"Fetch issue #123 from the `my-org/my-repo` repository and query the `my_table` table to find related data. Interpret the results and summarize the information."_ This process is much simpler, more secure, and less error-prone. You can also easily switch to a different AI agent that supports MCP without needing to rewrite your integrations. ## The Future of MCP It is still early days for MCP, but the community is rapidly building out the ecosystem. The specification is changing, and new features are being added to the SDKs. Here are some areas where we expect to see significant growth in Q3 of 2025: ### Client-server feature parity As the MCP Specification matures, we expect to see more features implemented in both the MCP server and MCP client SDKs. This will make it easier to build and use MCP servers, improving the overall developer experience. The current state of MCP implementations shows some gaps between what the specification defines and what the SDKs support. As these gaps close, developers will have access to a more complete and consistent toolset across different languages and platforms. We're already seeing rapid iteration on the TypeScript SDK, which serves as the reference implementation. As patterns emerge and stabilize there, we expect to see similar improvements in the Python, Go, and other language SDKs. ### Tool and resource discovery The MCP Specification enables much more dynamic tool and resource discovery. We expect to see more servers implementing this feature, allowing clients to use fewer tokens when providing context to LLMs. Dynamic discovery is already possible today, but many servers still register all their tools at initialization and never update them. As the ecosystem matures, we'll see more sophisticated implementations that: - Add and remove tools based on authentication state - Expose different capabilities based on user permissions - Dynamically generate tools based on runtime configuration - Notify clients of changes without requiring reconnection This will make MCP servers more efficient and responsive, reducing the overhead of maintaining unused tools in the LLM's context. ### Security and access control As MCP servers become more widely used, we expect to see more focus on security and access control. The specification already includes OAuth 2.1 support, but this addresses only a small part of the security model. Better guardrails against prompt injection and other attacks will be needed as the ecosystem grows. This is, unfortunately, a common theme in the LLM space, and perhaps one that can't be solved completely. It isn't unique to MCP, though. We anticipate several security-related developments: - **Sandboxing and containerization**: More tools like Docker's MCP Toolkit that run servers in isolated environments - **Permission models**: Granular controls over what tools can access and modify - **Audit logging**: Better tracking of tool invocations and data access - **Security scanning**: Automated tools to detect common vulnerabilities in MCP servers - **Signature verification**: Trust chains for MCP server distribution and updates The community is already moving in this direction, but we expect to see more standardization and tooling support in the coming months. ### Registries and marketplaces We expect to see more MCP server registries and marketplaces where developers can share and discover MCP servers. This will make it easier to find and use existing servers, as well as to contribute to the ecosystem. A trusted registry would also help with security, as users could verify the authenticity of the MCP servers they connect to. Think npm or PyPI, but for MCP servers - with package verification, versioning, dependency management, and security scanning. Early registries like [mcp.so](https://mcp.so/) show the demand for discoverability, but we need more robust infrastructure to support enterprise adoption. This includes: - Versioned releases with semantic versioning - Dependency resolution for servers that depend on other servers - Security scanning and vulnerability alerts - Usage analytics and ratings - Official/verified badges for trusted publishers - Private registries for enterprise deployments ### Cross-platform standardization As MCP gains adoption across different LLM providers and platforms, we expect to see more effort toward ensuring consistent behavior across implementations. Currently, different MCP clients may handle the same server differently, leading to fragmentation. The community will need to develop: - Comprehensive test suites for MCP compliance - Conformance testing tools for both servers and clients - Reference implementations for common use cases - Documentation of best practices and anti-patterns ### Integration with existing ecosystems MCP doesn't exist in isolation. We expect to see deeper integration with existing developer tools and workflows: - **IDE integrations**: Better support in VS Code, JetBrains IDEs, and other editors - **CI/CD pipelines**: Testing and deployment tooling for MCP servers - **Monitoring and observability**: Integration with APM tools and logging platforms - **Configuration management**: Tools for managing MCP server configurations at scale - **Package managers**: First-class support in language-specific package ecosystems ##The verdict Whether MCP will become the de facto standard for LLM tool integration remains to be seen. It is useful now, and it is likely to become more useful in the near future. But who knows what this space will look like in a year or two? Your guess is as good as ours. The protocol has genuine strengths - its dynamic discovery, stateful architecture, and standardized communication model solve real problems. But it also has real complexity that may not be justified for many use cases. The ecosystem's success will depend on: - How quickly the tooling matures to abstract away complexity - Whether major LLM providers adopt MCP broadly - The development of security best practices and tooling - The emergence of a healthy marketplace of reusable servers - Competition from simpler alternatives What's clear is that the problem MCP tries to solve - standardizing how AI agents interact with tools and services - isn't going away. Whether MCP is the solution, or whether something else emerges, the need for better agent-tool integration will only grow as AI capabilities expand. For now, the best approach is pragmatic: Use MCP where its benefits clearly outweigh its complexity, keep an eye on the ecosystem's evolution, and maintain flexibility to adapt as the landscape changes. ## How to get started with MCP Ready to start using MCP? Here are your next steps for: - **Using existing MCP servers**: [Installing MCP servers](/mcp/using-mcp/installing-mcp-servers) - **Building AI agents**: [Introduction to AI agents](/mcp/ai-agents/introduction) - **Creating MCP servers**: [Protocol deep dive](/mcp/building-servers/protocol-deep-dive) # MCP release notes Source: https://speakeasy.com/mcp/release-notes import { Callout, CodeWithTabs, Table } from "@/mdx/components"; Each release of the Model Context Protocol (MCP) introduces changes to how LLM applications integrate external tools and context. These updates can improve developer experience, interoperability, and safety, but they may also introduce breaking changes or require adjustments in your implementation. This page is not an official MCP release notes page. It exists to help developers understand what changed, why it matters, and how to adopt the latest MCP revisions. For the full changelog, refer to the [official changelog](https://github.com/modelcontextprotocol/modelcontextprotocol/tags). ## MCP version history This section summarizes the significant differences between MCP versions. The following table summarizes major changes between MCP versions.
## MCP 2025-06-18 vs MCP 2025-03-26 The **2025-06-18** MCP release represents a significant refinement of the Model Context Protocol, introducing structured tool outputs, enhanced OAuth security, and server-initiated user interactions. However, it also removes JSON-RPC batching from the previous version, simplifying protocol implementation while adding new capabilities for better security and data handling. Key highlights include: - **Removal of JSON-RPC batching:** The batching feature introduced in 2025-03-26 has been removed, simplifying the protocol implementation - **Structured tool output:** Tools can now return structured, predictable data formats for better integration - **Enhanced OAuth security:** MCP servers are now classified as OAuth Resource Servers with additional security requirements - **Elicitation capability:** Servers can now request additional information from users during interactions - **Resource links:** Tool results can now reference MCP resources, creating better integration between tools and data You may choose to remain on the **2025-03-26** version if you need: - **JSON-RPC batching support:** If your implementation relies on batching multiple requests - **Simpler OAuth implementation:** The earlier version has less stringent OAuth requirements ## What's new in MCP 2025-06-18? MCP **2025-06-18** introduces structured tool outputs, removes JSON-RPC batching, enhances OAuth security with Resource Server requirements, and adds elicitation for server-initiated user requests. Here's a detailed breakdown of the major updates: ### Removal of JSON-RPC Batching One of the most significant changes is the **removal of JSON-RPC batching** support, which was introduced in 2025-03-26. This decision simplifies the protocol implementation and reduces complexity for both clients and servers. The maintainers mentioned that there was ['no compelling use case for batching'](https://github.com/modelcontextprotocol/modelcontextprotocol/pull/416), but not all users agreed. If you were using batching in your 2025-03-26 implementation, you'll need to refactor to send individual requests instead of batched ones. ### Structured Tool Output The new [structured tool output](https://modelcontextprotocol.io/specification/2025-06-18/server/tools#structured-content) capability allows tools to return data in predictable, structured formats rather than just plain text responses. **What you need to change:** Update your tool definitions to specify `Tool.outputSchema` and return `CallToolResult.structuredContent` in your tool implementations. CallToolResult: """Get structured user information""" user_data = { "user": { "id": user_id, "name": "John Doe", "email": "john@example.com", "status": "active" } } return CallToolResult( content=[{"type": "text", "text": f"Retrieved user {user_id}"}], # New: Use structuredContent for predictable data format structuredContent=user_data ) # Set the tool's outputSchema property get_user_info.outputSchema = { "type": "object", "properties": { "user": { "type": "object", "properties": { "id": {"type": "string"}, "name": {"type": "string"}, "email": {"type": "string"}, "status": {"type": "string"} } } } } # Register tool with outputSchema @server.list_tools() async def handle_list_tools() -> list[Tool]: return [ Tool( name="get_user_info", description="Get structured user information", inputSchema={ "type": "object", "properties": { "user_id": {"type": "string"} }, "required": ["user_id"] }, # New: Add outputSchema to specify return format outputSchema={ "type": "object", "properties": { "user": { "type": "object", "properties": { "id": {"type": "string"}, "name": {"type": "string"}, "email": {"type": "string"}, "status": {"type": "string"} } } } } ) ]`, }, { label: "TypeScript", language: "typescript", code: `// Using official TypeScript SDK - @modelcontextprotocol/sdk package import { Server } from "@modelcontextprotocol/sdk/server/index.js"; import { CallToolRequestSchema, CallToolResult, ListToolsRequestSchema, Tool, } from "@modelcontextprotocol/sdk/types.js"; const server = new Server( { name: "example-server", version: "1.0.0" }, { capabilities: { tools: {} } }, ); // Handle tool calls with structured output server.setRequestHandler(CallToolRequestSchema, async (request) => { const { name, arguments: args } = request.params; if (name === "get_user_info") { const userData = { user: { id: args.user_id, name: "John Doe", email: "john@example.com", status: "active", }, }; return { content: [{ type: "text", text: \`Retrieved user \${args.user_id}\` }], // New: Use structuredContent for predictable data format structuredContent: userData, } as CallToolResult; } }); // Register tool with outputSchema server.setRequestHandler(ListToolsRequestSchema, async () => { return { tools: [ { name: "get_user_info", description: "Get structured user information", inputSchema: { type: "object", properties: { user_id: { type: "string" }, }, required: ["user_id"], }, // New: Add outputSchema to specify return format outputSchema: { type: "object", properties: { user: { type: "object", properties: { id: { type: "string" }, name: { type: "string" }, email: { type: "string" }, status: { type: "string" }, }, }, }, }, } as Tool, ], }; });`, } ]} /> ### Enhanced OAuth Security MCP servers are now classified as [OAuth Resource Servers](https://modelcontextprotocol.io/specification/2025-06-18/basic/authorization#authorization-server-discovery), introducing two **MUST** requirements for both servers and clients: 1. **Resource Server Discovery:** Servers **MUST** provide metadata to help clients discover the corresponding Authorization Server 2. **Resource Indicators:** Clients **MUST** implement [RFC 8707](https://www.rfc-editor.org/rfc/rfc8707.html) Resource Indicators to prevent malicious servers from obtaining inappropriate access tokens **What you need to change:** Update your server to expose authorization metadata and modify your client to include resource indicators in token requests. TokenInfo: # MUST: Validate token is scoped to this resource # Verify with your authorization server via token introspection response = await introspect_token(token) if not response.get("active"): raise ValueError("Token is not active") # Verify resource scope if "https://api.example.com/" not in response.get("aud", []): raise ValueError("Token not valid for this resource") return TokenInfo( sub=response["sub"], scopes=response["scope"].split(), expires_at=response["exp"] ) # Create server with OAuth Resource Server configuration mcp = FastMCP( "example-server", token_verifier=MyTokenVerifier(), auth=AuthSettings( # MUST: Expose authorization server discovery metadata issuer_url="https://auth.example.com", resource_server_url="https://api.example.com/", required_scopes=["read", "write"], ), ) async def introspect_token(token: str) -> dict: # Your token introspection implementation # Typically calls your authorization server's introspection endpoint pass`, }, { label: "Python Client", language: "python", code: `# Using official Python SDK - mcp package from mcp.client.auth import OAuthClientProvider, TokenStorage from mcp.client.session import ClientSession from mcp.client.streamable_http import streamablehttp_client from mcp.shared.auth import OAuthClientInformationFull, OAuthClientMetadata, OAuthToken class SecureTokenStorage(TokenStorage): async def get_tokens(self) -> OAuthToken | None: # Load tokens from secure storage pass async def set_tokens(self, tokens: OAuthToken) -> None: # Save tokens to secure storage pass async def get_client_info(self) -> OAuthClientInformationFull | None: # Load client registration info pass async def set_client_info(self, client_info: OAuthClientInformationFull) -> None: # Save client registration info pass async def connect_to_protected_server(): # Set up OAuth authentication with Resource Indicators oauth_auth = OAuthClientProvider( server_url="https://api.example.com/", # MUST: Specify target resource client_metadata=OAuthClientMetadata( client_name="My MCP Client", redirect_uris=["http://localhost:3000/callback"], grant_types=["authorization_code", "refresh_token"], response_types=["code"], ), storage=SecureTokenStorage(), redirect_handler=lambda url: print(f"Visit: {url}"), callback_handler=lambda: ("auth_code", None), ) # Connect with resource-scoped authentication async with streamablehttp_client( "https://api.example.com/mcp", auth=oauth_auth ) as (read, write, _): async with ClientSession(read, write) as session: await session.initialize() # Authenticated session ready with resource-scoped tokens`, } ]} /> ### Security Best Practices The 2025-06-18 release introduces comprehensive [security best practices](https://modelcontextprotocol.io/specification/2025-06-18/basic/security_best_practices) documentation. This covers token validation, resource isolation, and threat mitigation to help prevent common security vulnerabilities in MCP implementations. ### MCP Elicitation Support The new [elicitation](https://modelcontextprotocol.io/specification/2025-06-18/client/elicitation) capability enables servers to request additional information from users during interactions. Elicitation allows servers to pause tool execution and ask users for specific information using structured JSON schemas. For example, a booking tool might request travel dates, or a file processor might ask for output format preferences. **What you need to change:** Enable the `elicitation` capability and use `context.elicit()` to request user input when your tools need additional information. list[types.TextContent | types.ImageContent | types.EmbeddedResource]: if name == "book_flight": departure = arguments.get("departure") destination = arguments.get("destination") # If departure city is missing, ask the user if not departure: result = await context.elicit( message="What city are you departing from?", schema={ "type": "object", "properties": { "departure": {"type": "string", "description": "Departure city"} }, "required": ["departure"] } ) if result.action == "accept": departure = result.content["departure"] else: return [types.TextContent(type="text", text="Flight booking cancelled")] # Continue with booking... return [types.TextContent( type="text", text=f"Booking flight from {departure} to {destination}" )]`, }, { label: "TypeScript", language: "typescript", code: `server.setRequestHandler(CallToolRequestSchema, async (request, context) => { const { name, arguments: args } = request.params; if (name === "book_restaurant") { const restaurant = args.restaurant; const date = args.date; const partySize = args.partySize; // Check availability const isAvailable = await checkAvailability(restaurant, date, partySize); if (!isAvailable) { // Ask user if they want to check alternatives const result = await context.elicit({ message: \`No availability at \${restaurant} on \${date}. Would you like to check alternative dates?\`, requestedSchema: { type: "object", properties: { checkAlternatives: { type: "boolean", title: "Check alternative dates", description: "Would you like me to check other dates?", }, flexibleDates: { type: "string", title: "Date flexibility", description: "How flexible are your dates?", enum: ["next_day", "same_week", "next_week"], enumNames: ["Next day", "Same week", "Next week"], }, }, required: ["checkAlternatives"], }, }); if (result.action === "accept" && result.content?.checkAlternatives) { const alternatives = await findAlternatives( restaurant, date, partySize, result.content.flexibleDates as string, ); return { content: [ { type: "text", text: \`Found these alternatives: \${alternatives.join(", ")}\`, }, ], }; } return { content: [ { type: "text", text: "No booking made. Original date not available.", }, ], }; } // Book the table await makeBooking(restaurant, date, partySize); return { content: [ { type: "text", text: \`Booked table for \${partySize} at \${restaurant} on \${date}\`, }, ], }; } });`, } ]} /> Key features: - **Structured requests:** Use JSON schemas to define exactly what information you need - **User control:** Users can accept, reject, or cancel elicitation requests - **Simple schemas:** Only primitive types (string, number, boolean) are supported for easier client implementation - **Security:** Servers must not request sensitive information through elicitation ### Resource Links in Tool Results Tools can now return [resource links](https://modelcontextprotocol.io/specification/2025-06-18/server/tools#resource-links) that reference MCP resources, creating better integration between tool outputs and available data sources. ### MCP Protocol Version Headers HTTP requests now require the `MCP-Protocol-Version` header to enable [version negotiation](https://modelcontextprotocol.io/specification/2025-06-18/basic/transports#protocol-version-header). **Clients are responsible** for setting this header in all HTTP requests after the initial version negotiation is completed during the handshake. **What you need to change:** Update your HTTP client to include the `MCP-Protocol-Version` header in all requests after connecting. ```http POST /mcp HTTP/1.1 Host: example.com Content-Type: application/json MCP-Protocol-Version: 2025-06-18 { "jsonrpc": "2.0", "method": "tools/call", "params": { ... } } ``` ### Title Fields for Better UX The `title` field has been added to tools, resources, and prompts to provide human-friendly display names while keeping `name` as the programmatic identifier. This improves how MCP items appear in client interfaces. **What you need to change:** Add `title` fields to your tools, resources, and prompts for better user experience in clients like VS Code Copilot Chat. In practice, this means users see "Search repositories" instead of "search_repositories" in their IDE: ```python # Python SDK example @mcp.tool(title="Search repositories") def search_repositories(query: str, language: str = None) -> str: """Search for GitHub repositories""" # name remains "search_repositories" for programmatic access # title shows "Search repositories" in UIs return f"Searching repositories for: {query}" @mcp.resource("search://{query}", title="Repository Search Results") def get_search_results(query: str) -> str: """Get repository search results""" return f"Search results for {query}" ``` Here's how it looks in VS Code Copilot Chat: ![Search repositories in VS Code](/assets/mcp/release-notes/release-notes-search-repositories.png) ### Other Notable Changes - **Lifecycle Operations:** Changed from **SHOULD** to **MUST** for certain [lifecycle operations](https://modelcontextprotocol.io/specification/2025-06-18/basic/lifecycle#operation) - **Meta Fields:** Added `_meta` field to additional interface types for better extensibility - **Completion Context:** Added `context` field to `CompletionRequest` for LLM-based autocompletion support ## MCP 2025-03-26 vs MCP 2024-11-05 The **2025-03-26** release of the Model Context Protocol introduces a more mature and standardized approach to authorization, transport, and tool metadata. It brings significant improvements in security, flexibility, and client compatibility, but it also includes breaking changes that may require updates in existing implementations if you are moving from the **2024-11-05** version. Key highlights include: - **Structured OAuth-style authorization:** Authorization is now defined explicitly using a consistent schema, making supporting various auth models and token types easier. - **Streamable HTTP transport:** The previous HTTP with SSE model has been replaced with a more robust, proxy-friendly HTTP streaming format. - **JSON-RPC batching:** Developers can now bundle multiple requests in a single call, improving efficiency in complex workflows (note: this was later removed in 2025-06-18). - **Tool annotations:** Tools can now describe their behavior (for example, `read-only`, `destructive`), allowing for safer and more user-aware execution. You may choose to remain on the **2024-11-05** version if you need: - **SSE-based transport compatibility:** If you're invested in SSE infrastructure and can't yet support chunked HTTP streaming. - **Minimal tooling overhead:** The earlier version uses simpler schemas and fewer requirements, which may suit prototypes or early integrations. ## What's new in 2025-03-26? MCP **2025-03-26** introduces the following changes: Authorization framework inspired by OAuth 2.1. and Streamable HTTP replacing HTTP with SSE. ### Authorization The **2024-11-05** version of MCP doesn't include a standardized authorization model. As a result, developers must implement their own authorization schemes, which leads to inconsistent behaviors and security risks, as tokens are passed directly in the input parameters. The new model supports role-based access control, dynamic client registration, and standard PKCE-based flows. It also allows for better error handling and smoother integration with existing identity providers. For example, you might define a tool to make bank transfers as follows: ```python from mcp.server import Server from mcp.auth import TokenValidator from apis import banking_api app = Server("bank-transfer") token_validator = TokenValidator() @app.tool("initiateTransfer", description="Send money to another account", args={"amount": int, "recipient": str}) async def initiate_transfer(amount: int, recipient: str, context): # Require authentication token = context.auth.token if not token: raise PermissionError("Authentication required") # Check if user has permission to transfer funds user_roles = token_validator.get_roles(token) if "finance-admin" not in user_roles: raise PermissionError("Only finance admins can initiate transfers") # Proceed with the transfer result = await banking_api.transfer_funds(amount, recipient) return {"status": result.status, "recipient": recipient, "amount": amount} ``` Learn more about MCP authorization in the [MCP authorization documentation](/mcp/securing-mcp-servers/authorizing-mcp-servers). ### Streamable HTTP transport In MCP **2024-11-05**, long-lived connections are handled using server-sent events (SSE). SSE supports server-to-client streaming, but client-to-server communication relies on HTTP POST requests. This means managing two different layers of communication, leading to avoidable complexity in coding and implementation. With version **2025-03-26**, MCP introduces a new transport layer that uses chunked HTTP streaming, replacing SSE. Let's compare the implementation of a streamable HTTP server to the previous SSE-based server. Here is an example of a simple server using SSE: ```python from mcp.server import Server from mcp.server.sse import SseServerTransport from starlette.applications import Starlette from starlette.routing import Route app = Server("bank-transfer") sse = SseServerTransport("/messages") @app.tool("initiateTransfer", args={"amount": int, "recipient": str}) async def initiate_transfer(amount: int, recipient: str, context): return { "status": "transferred", "recipient": recipient, "amount": amount } # SSE handler: for persistent GET connection async def sse_handler(scope, receive, send): async with sse.connect_sse(scope, receive, send) as streams: await app.run(streams[0], streams[1], app.create_initialization_options()) # POST handler: for sending requests async def post_handler(scope, receive, send): await sse.handle_post_message(scope, receive, send) starlette_app = Starlette(routes=[ Route("/sse", sse_handler), Route("/messages", post_handler, methods=["POST"]) ]) ``` Here is the equivalent server using Streamable HTTP: ```python from mcp.server import Server from mcp.server.streamable_http_manager import StreamableHTTPSessionManager from starlette.applications import Starlette from starlette.routing import Mount from starlette.types import Scope, Receive, Send app = Server("bank-transfer") session_manager = StreamableHTTPSessionManager(app=app) @app.tool("initiateTransfer", args={"amount": int, "recipient": str}) async def initiate_transfer(amount: int, recipient: str, context): return { "status": "transferred", "recipient": recipient, "amount": amount } # Streamable HTTP handler async def handle_streamable_http(scope: Scope, receive: Receive, send: Send): await session_manager.handle_request(scope, receive, send) starlette_app = Starlette( routes=[ Mount("/mcp", app=handle_streamable_http) ] ) ``` Here are some reasons you should consider switching to Streamable HTTP: - **Proxy and load balancer compatibility:** SSE often breaks under proxies due to connection timeouts and buffering limits. For example, AWS ALB and Cloudflare frequently terminate SSE connections, while Streamable HTTP uses standard chunked encoding and works reliably. - **Full-duplex communication:** SSE only supports server-to-client messages, requiring separate POSTs for client input. Streamable HTTP enables bidirectional streaming over one connection, simplifying cases like real-time tool execution or chat interfaces. ### Other notable changes Apart from the improvements to authorization and HTTP streaming transport, MCP **2025-03-26** introduces these changes: - **Tool calls can now be batched:** Clients may send multiple tool invocations in a single JSON-RPC request. Servers that assume one call per request must handle arrays and concurrent execution. (Note: This feature was removed in 2025-06-18) - **Tool behavior must now be declared explicitly:** Tools can define attributes like `readOnly` or `destructive`, which clients are expected to respect. This may impact how tools are displayed or gated in UIs. - **Progress updates are now more descriptive:** The `ProgressNotification` object includes an optional `message` field. Clients that display progress may want to surface these descriptions to users. - **Audio is a supported content type:** Servers and clients must now handle `audio` in addition to `text` and `image`. - **Tools can advertise completion support:** Servers can declare that a tool supports input autocompletion. You may need to update your UI to initiate and render completion suggestions accordingly. Learn more about changes to the protocol on the [MCP 2025-03-26 changelog page](https://modelcontextprotocol.io/specification/2025-03-26/changelog). # Secure your MCP server Source: https://speakeasy.com/mcp/securing-mcp-servers MCP servers handle requests from LLM agents, not users. When a user calls your API, you have a good idea of what they're requesting. When an LLM agent calls your MCP server, you're trusting the agent's interpretation of what the user wanted. This creates new attack vectors. An attacker doesn't need to compromise your server directly. They could inject malicious instructions into content the LLM reads, and the LLM would execute those instructions, thinking it's following the user's intent. This guide explains the threats facing MCP servers and shows you which security controls to implement and why, covering authentication and authorization, credential protection, prompt‑injection defenses, encrypted transport, and monitoring. ## Understanding MCP security threats MCP servers face unique threats because LLM agents sit between users and your tools. Attackers can exploit this intermediary to bypass traditional security controls. ### Prompt injection attacks In a prompt injection attack, an attacker uses malicious prompts to manipulate an LLM into performing harmful acts. They can do this in two ways: - **In a direct prompt injection,** the attacker delivers prompts through input fields in the AI application. - **In an indirect prompt injection,** the attacker hides malicious prompts in content that LLMs consume, such as images and PDF documents. Many prompt injection methods don't require any specialist knowledge or even coding ability. Attackers need only a basic understanding of how LLMs function, what their vulnerabilities are, and where to insert malicious prompts effectively. For example, an attacker could execute a prompt injection as follows: - The attacker poisons the document. - The MCP server retrieves the document. - The LLM reads the malicious prompt. - The LLM calls an unauthorized tool. - Data is exfiltrated. ![Prompt injection via document](/assets/mcp/securing-your-mcp-server/prompt-injection-attack.png) ### Unauthorized access Without proper authentication, anyone can connect to your MCP server and call your tools. Attackers can gain access to your MCP server as a result of: - **Missing authentication:** Your server accepts tool calls from any clients without verifying their identities. - **Weak authentication:** API keys passed through prompts can be intercepted or leaked through logs. - **Credential theft:** Attackers steal valid tokens and impersonate legitimate users. ### Data exfiltration Compromised agents can extract sensitive data through legitimate tool calls using: - **High-frequency queries:** An agent makes thousands of database queries in minutes, extracting customer records. - **Unauthorized tool access:** A support agent's session unexpectedly initiates calls to admin-only tools to access restricted data. - **Prompt-triggered exfiltration:** Malicious instructions in retrieved content tell the LLM to send data to external URLs. With these threats in mind, a robust authentication and authorization system is the first control you should put in place. ## Implement authentication and authorization Authentication controls who can access your server, and authorization controls what they can do once they have connected. ### Use OAuth 2.1 for authentication MCP servers connect users to external APIs. You could accept API keys directly, but this creates three problems: - **Tokens in prompts are vulnerable:** Attackers intercept credentials through man-in-the-middle attacks. When tokens leak through logs, your server becomes a tool for credential theft. - **Users struggle to manage scopes correctly:** Users don't know which permissions your tools require and either grant excessive permissions, creating a security risk, or grant insufficient permissions, causing tools to break. - **API keys are long-lived:** If compromised, attackers have access to the server until the key is manually revoked. OAuth 2.1 fixes all three issues: - **Vulnerable tokens in prompts:** Authentication no longer occurs within the LLM context. Instead, users authenticate directly with your OAuth provider (for example, Auth0, Okta, or FusionAuth). - **User scope management errors:** You now define the required scopes, not your users. - **Long-lived API keys:** Tokens are now short-lived and expire automatically, and you can revoke access to your server without requiring users to regenerate their keys. MCP integrates with standard OAuth 2.1. The high‑level flow in an MCP context looks like this: - The MCP client (the AI) requests a protected tool, and your MCP server returns `401` with an OAuth authorization URL, triggering the authentication flow. - The user completes the OAuth flow, authenticating with your OAuth provider (for example, Auth0 or Okta) and granting permissions for specific scopes. - The MCP client receives an access token. - The MCP client securely stores the token and includes it in subsequent requests as a Bearer token. - On each tool call, your MCP server validates the token against the OAuth provider using token introspection. - Your MCP server uses the validated user information to call the tool with the appropriate user context (permissions and attribution). The key way OAuth 2.1 differs from traditional OAuth is that, instead of the user manually initiating login flows, the AI model triggers the authentication flow when it needs to access protected tools. ![OAuth 2.1 flow](/assets/mcp/securing-your-mcp-server/oauth-flow.png) Learn how to add OAuth 2.1 to your MCP server in the Gram guide to [building MCP servers with external OAuth](/docs/gram/examples/oauth-external-server). ### Encrypt communications with mutual TLS Standard TLS (HTTPS) authenticates only the server. The client knows it's talking to the real server, but the server doesn't know who the client is. Mutual TLS (mTLS) authenticates both the server and the client. They both present certificates and verify each other's identity before exchanging data. This prevents unauthorized clients from connecting to your MCP server and prevents man-in-the-middle attacks. | Without mTLS | With mTLS | | ---------------------------------------------------------------- | ---------------------------------------------------------- | | Anyone can attempt to connect to your server | Only clients with valid certificates can connect | | Client identity verified at application level (API keys, tokens) | Client identity verified at transport layer (certificates) | | API keys can be stolen and reused | Certificates require both public cert and private key | | No cryptographic proof of client identity | Cryptographic verification of both parties | | Logging shows IP addresses | Logging shows verified client identities | You can set up mTLS for a Python MCP server using uvicorn: ```python import ssl import uvicorn # Create SSL context ssl_context = ssl.SSLContext(ssl.PROTOCOL_TLS_SERVER) # Load server certificate and private key ssl_context.load_cert_chain("server.crt", "server.key") # Load CA certificate to verify clients ssl_context.load_verify_locations("client_ca.crt") # Require client certificates ssl_context.verify_mode = ssl.CERT_REQUIRED # Run server with mTLS uvicorn.run("app:app", ssl_context=ssl_context) ``` For optimal protection, combine mTLS with OAuth 2.1: - **mTLS** operates at the transport layer, ensuring the identities of the client and the server and establishing a secure channel between them. - **OAuth 2.1** operates at the application layer, providing granular, policy-driven authorization and delegation of access. By combining them, you can build a more secure MCP server that protects against a broader range of threats, such as token replay and man-in-the-middle attacks. ### Choose your authentication model Your authentication approach depends on your use case. #### Pass-through authentication In pass-through authentication, users provide their own API keys or credentials directly to your MCP server. The server uses these credentials to access external services on behalf of the user. Never accept credentials through prompts. If you build an MCP server that accepts API tokens directly from LLM prompts, you create multiple attack vectors: - Man-in-the-middle attacks can intercept credentials in transit. - Credentials can leak through logging or monitoring systems. - Your server can become an explicit attack tool for credential theft. If you use pass-through authentication, accept credentials only through: - Environment variables - Secure configuration files outside the MCP context - OAuth flows (recommended) #### Managed authentication In managed authentication, you handle user credentials on the server side. Users don't provide their own API keys; you manage access control and use your own service accounts to perform actions on their behalf. This works best for: - Internal enterprise tools, where you control who has access - Situations where you strictly control the actions that users can perform - APIs that don't require individual user credentials aren't necessary ## Apply foundational security controls Before exploring specific practices to address specific threats, it is essential to establish foundational controls that work together to mitigate the impacts when attacks succeed. ### Grant minimum necessary permissions Excessive permissions expand your attack surface. A compromised search tool with write access can modify or delete data it should only read. Grant each tool only the permissions required for its function. A customer search tool needs read access, not the ability to modify or delete customer records. A customer search tool should request, for example, only `customers:read` permissions: ```json { "name": "search_customers", "description": "Search customer database", "permissions": ["customers:read"] } ``` It shouldn't request full CRUD access: ```json { "name": "search_customers", "description": "Search customer database", "permissions": [ "customers:read", "customers:write", "customers:delete", "billing:read" ] } ``` To apply the principle of least privilege on your MCP server, you need to: - Determine the minimum access each tool needs to function - Strip away any extra permissions - Review and adjust permissions on a regular basis ### Restrict tool access by client role Limit which tools each client can use based on their role. A customer support agent doesn't need access to billing modification or database deletion tools. ### Mask sensitive data When attackers exfiltrate data through prompt injection, masked values protect your credentials and customer information. Replace sensitive data (such as social security numbers, API keys, and passwords) with substitutes before transmission. For example, data may look like this before masking: ```json { "customer": { "name": "John Smith", "ssn": "123-45-6789", "email": "john.smith@email.com", "api_key": "sk_live_abc123xyz456" } } ``` The same data should look like this after masking: ```json { "customer": { "name": "John Smith", "ssn": "XXX-XX-6789", "email": "j***@email.com", "api_key": "[REDACTED]" } } ``` ### Never expose secrets in LLM-readable content How you handle credentials determines whether your MCP server is a security asset or a liability. The first rule is to **never put secrets where LLMs can read them,** such as in: - Tool descriptions and metadata - Tool output messages - Error messages - Documentation strings - Example parameters - Configuration files that are returned to clients ## Defend against prompt injection To defend against prompt injection, you should: - **Apply input filtering:** Strip or flag suspicious patterns in user-supplied text before it reaches your model. For example, you can filter inputs in Python as follows: ```python def sanitize_input(user_input: str) -> str: suspicious_patterns = [ 'ignore previous instructions', 'disregard your rules', 'system prompt' ] for pattern in suspicious_patterns: if pattern in user_input.lower(): raise SecurityException("Suspicious input detected") return user_input ``` - **Scan retrieved content:** Check all data from MCP servers for obvious payloads like script tags, tool command patterns, SQL queries, or suspicious phrases. - **Monitor for unusual behavior:** Watch out for agents attempting to access or share restricted data. ## Monitor and detect threats Without monitoring, attackers can operate in your system undetected. Monitoring helps you catch attacks in progress. Log every tool call with structured data, including the invocation ID, timestamp, client ID, tool name, and parameters, so that you can watch for: - Unusual tool access patterns, such as a support agent suddenly using admin tools - High-frequency API calls suggesting data exfiltration - Failed authentication attempts - Tools being called with unexpected parameter patterns To learn more about performance metrics, alerting strategies, and setting up production monitoring, see our guide to [monitoring MCP servers](/mcp/monitoring-mcp-servers). ## Final thoughts In addition to foundational security controls like data masking and least privilege, you should protect MCP servers in production with OAuth 2.1 (and mTLS for enterprise deployments), credential management, and prompt injection defenses. Use the following checklist to put these recommendations into action: - Implement OAuth 2.1 for authentication and scope management. - Enforce mutual TLS (mTLS) where you need strong client authentication. - Apply the principle of least privilege using minimal tool permissions and role‑based access. - Never expose secrets to LLM‑readable content; use managed secrets and masking. - Add prompt‑injection defenses to inputs and retrieved content. - Log all tool calls and monitor them for anomalies. Securing your MCP server isn't a one-off task. Threats evolve as attackers discover new techniques targeting LLM-based systems, so [monitoring](/mcp/monitoring-mcp-servers) your MCP server is crucial for identifying threats and taking defensive measures as quickly as possible. Looking for a platform that hosts MCP servers with built-in SOC 2 compliance? [Gram](https://getgram.ai/) provides managed MCP infrastructure, allowing you to focus on curating tools instead of managing security. # Authenticating MCP servers: a work in progress Source: https://speakeasy.com/mcp/securing-mcp-servers/authenticating-mcp-servers import { Callout } from "@/lib/mdx/components"; The Model Context Protocol (MCP) Specification is clear about what is expected from MCP servers to implement OAuth support. Those words may seem reassuring, especially if you already implement OAuth in your products. However, your OAuth server most likely doesn't comply with MCP's requirements. The MCP Specification expects authorization servers to meet a highly specific set of OAuth features that many OAuth servers do not support. This mismatch between current OAuth implementations and what MCP expects is a common point of friction holding back enterprise adoption of MCP. If you're still in the exploratory phase of MCP development for your product, you may not have bumped into this problem yet. The OAuth situation usually only rears its head when you get to the point at which you consider the practical steps a user will take when authenticating with your service and authorizing the MCP server to act on their behalf. In this guide, we'll explain this mismatch in plain terms, offer alternatives, and hopefully help you find your way. ## The perfect world that doesn't exist In an ideal universe, here's how MCP OAuth would work: Your company already has an OAuth server[^1] that perfectly complies with the MCP Specification. All you need to do is host a well-formed `/.well-known/oauth-protected-resource` file on your MCP server, and you're done! The MCP client handles the entire OAuth flow automatically. While the MCP Specification technically describes using `/.well-known/oauth-protected-resource` for discovery, most current MCP clients (including Claude itself) actually look for `/.well-known/oauth-authorization-server` directly at the MCP server's domain. This is how the OAuth dance works in practice today. The gap between specification and implementation is prevalent in the MCP ecosystem. In reality, however, only a handful of OAuth providers have added support for the specific features required by the MCP Specification. Most OAuth providers would need to change their implementation significantly to support MCP's requirements. The MCP Specification requires your OAuth provider to support: - OAuth 2.1 with mandatory PKCE[^2] - **Dynamic Client Registration (DCR)**[^3] - Authorization Server Metadata - Protected Resource Metadata That second requirement, Dynamic Client Registration, is where everything falls apart. ## The DCR problem When creating an OAuth client, the usual workflow is for the developer to manually get a `client_id` and `client_secret` from the authorization server, then configure their application with these credentials. This traditional approach is aimed at developers, not end users. It assumes that the developer has access to the OAuth server's management interface and can create an application manually. The idea of end users finding your application's OAuth registration page, filling out a form, and getting a client ID and secret is not how the MCP Specification envisions OAuth. Instead, it expects that MCP clients can dynamically register themselves as OAuth clients without manual intervention. Here's the crux of the issue: MCP clients have no standardized way to handle client IDs or client secrets[^4]. They need to be able to register themselves dynamically as OAuth clients to perform the authentication dance. But here's the thing, and it cannot be stressed enough: **most OAuth providers do not support Dynamic Client Registration** (and yours probably doesn't either). Google? Nope. GitHub? Not a chance. Microsoft Azure AD? Don't even think about it. Your company's homegrown OAuth solution that Bob from IT built five years ago? Definitely not. If you're thinking, "But wait, I've seen MCP servers with OAuth!" you're right, but most are not doing what you think they're doing. ## The OAuth proxy solution There's a solution, but it isn't pretty. Since most OAuth providers don't support DCR, the community has come up with a workaround: **OAuth proxies**. An OAuth proxy sits between the MCP client and your actual OAuth provider. Think of it as a translator that speaks "MCP OAuth" on one side and "regular OAuth" on the other. The most popular implementation uses Cloudflare Workers[^5], which has become the de facto standard for this architectural pattern. Here's how it works: 1. The proxy exposes its own OAuth endpoints that comply with MCP requirements (including DCR support). 2. The MCP client only talks to the proxy, never directly to your real OAuth provider. 3. The proxy handles all the translation between MCP's expectations and your OAuth provider's reality. 4. Fake tokens all the way down: The tokens the MCP client receives are generated by the proxy; they never see your actual OAuth provider's tokens. The Cloudflare OAuth wrapper exposes endpoints like: - `/.well-known/oauth-authorization-server` - `/register` (for dynamic client registration) - `/authorize` - `/token` Behind the scenes, the proxy stores all the mapping data in Cloudflare KV[^6], maintains the relationship between its "fake" tokens and the real provider tokens, and handles all the complexity of token refresh and validation. This is quite a bit of responsibility for a proxy to take on, and the implications for your security posture are significant. You're essentially building an entire OAuth authorization server to bridge the gap between MCP's requirements and existing OAuth providers. ## The hidden complexity of OAuth proxies Building an OAuth proxy isn't just a weekend project. You need to handle: - **PKCE verification**[^7] (because MCP requires OAuth 2.1) - **Token lifecycle management** (storing, refreshing, and revoking) - **Security considerations** (you're now responsible for token security) - **Custom adapters** (for each OAuth provider you want to support) - **Performance optimization** (every auth check now has additional hops) The fun doesn't stop there. ## The illusion of Dynamic Client Registration Most current implementations store a single `client_id` and `client_secret` for the entire proxy. This is how both Cloudflare's proxy and Stytch currently work. Despite the technical implementation of Dynamic Client Registration within the proxy, from the upstream OAuth provider's perspective, there's only one registered application. This means every user of your MCP server is effectively acting under the same OAuth application. It's not true _Dynamic Client Registration_ in the sense that each MCP client gets its own distinct OAuth application identity with the upstream provider. Depending on your use case, this might be fine, or it might be a compliance nightmare. Up until now, your customers have been using your OAuth server with their own client IDs and secrets; but now they all share the same credentials. This can lead to issues with rate limiting, token revocation, and auditing. Simply plopping a Cloudflare OAuth proxy in front of your MCP server doesn't magically solve these problems. You may need to make significant changes to your own systems to accommodate this new architecture. ## MCP OAuth example: WorkOS and Cloudflare Workers The [Cloudflare AI repository](https://github.com/cloudflare/ai/tree/main/demos) provides several live examples demonstrating OAuth proxy implementations. The [WorkOS example](https://github.com/cloudflare/ai/blob/main/demos/remote-mcp-authkit/src/index.ts#L74) illustrates how an OAuth wrapper is registered using the Cloudflare provider, with [custom adapters](https://github.com/cloudflare/ai/blob/main/demos/remote-mcp-authkit/src/authkit-handler.ts) that enable the Cloudflare OAuth server to proxy requests to WorkOS. After analyzing the complete [Cloudflare provider source implementation](https://github.com/cloudflare/workers-oauth-provider/blob/main/src/oauth-provider.ts), several key architectural patterns emerge that are consistent across all examples: - The Cloudflare wrapper exposes its own complete OAuth, including `.well-known/oauth-authorization-server`, `register`, `authorize`, and `token` endpoints. The MCP client never directly interacts with the downstream OAuth provider and remains unaware of the actual provider's server details. - All server metadata returned in `well-known/oauth-authorization-server` point exclusively to the Cloudflare server. The authorization codes and access tokens received by the MCP client are proxy tokens generated by the Cloudflare wrapper, completely isolated from the underlying WorkOS tokens. - The Cloudflare provider uses Cloudflare KV for persistent storage of grant data, authorization codes, and provider access tokens. - Despite the proxy architecture, the proxy still redirects users to the authentic OAuth consent screen of the underlying provider. - The Cloudflare provider handles PKCE verification, ensuring OAuth 2.1 security standards are maintained throughout the proxy flow. - Custom WorkOS adapters integrate with the Cloudflare OAuth wrapper through `OAUTH_PROVIDER.parseAuthRequest` and `OAUTH_PROVIDER.completeAuthorization` functions. The `completeAuthorization` method allows the Cloudflare wrapper to complete a token exchange with the real OAuth server, store the resulting token in KV storage, and subsequently initiate the MCP client callback. The final token exchange occurs exclusively between the MCP client and the Cloudflare wrapper. - On the WorkOS side, you configure your Cloudflare MCP domain callback URL as a valid redirect URL in WorkOS and use your single WorkOS `ClientID` and `ClientSecret` in the server adapter. Regardless of how many MCP clients connect, they all funnel through one WorkOS OAuth application registration. ## How MCP servers use OAuth today So who is actually using OAuth with MCP servers today? The answer reveals a lot about the state of the ecosystem. Many companies claiming to support "OAuth" for their MCP servers are actually doing something much simpler. GitHub, for example, has extensive documentation about hosting MCP servers remotely. But when you get to the [authentication section](https://github.com/github/github-mcp-server/blob/main/docs/host-integration.md#authentication-and-authorization), you'll find this note: "Dynamic Client Registration is NOT supported by Remote GitHub MCP Server at this time." Instead, they expect users to obtain a personal access token separately and pass it directly in headers to their MCP server. This is OAuth in name only - there's no OAuth flow happening through the MCP client at all. This is actually a perfectly valid approach! Sometimes the simplest solution is best. But it's not what the MCP Specification envisions. The companies that are supporting true MCP OAuth are almost universally using the Cloudflare Workers proxy pattern or have built something similar themselves. ## The architectural implications of OAuth proxies Building an OAuth proxy means you're essentially building a distributed authorization server. Your proxy needs to handle all the stateful operations that OAuth requires: storing authorization codes, managing token lifecycles, and maintaining secure mappings between proxy tokens and real provider tokens. Every API call to your MCP server now involves additional hops through the proxy. Token validation that used to be a simple database lookup now involves multiple service calls. Security gets more complex. You're now responsible for securing the proxy infrastructure itself, protecting the token mapping database, ensuring proper token rotation and expiration, and all the edge cases that come with handling OAuth tokens. You also may need to update your logging and monitoring practices. When all users share the same OAuth application credentials, depending on your current setup, you may lose granular visibility into who's doing what. Your audit logs show one application making all the requests. These are not trivial changes. They require careful planning, additional resources, and a deep understanding of both OAuth and MCP. ## The developer experience gap This brings us to MCP auth's most difficult challenge: the mismatch between developer expectations and reality. Developers coming to MCP expect OAuth to work like it does everywhere else: get your client credentials, configure your app, and done. Instead, they find themselves in a quagmire of proxy architectures, token mapping strategies, and the intricacies of DCR. What should be a simple authentication problem has turned into an architectural redesign that touches security, performance, and compliance. ## Looking forward The MCP OAuth situation represents a fundamental tension between the protocol's vision of seamless, user-centric authentication and the realities of existing OAuth infrastructure. The specification's requirements, while well-intentioned, create significant implementation barriers for teams looking to build on MCP. For now, teams implementing MCP need to carefully evaluate their authentication requirements and choose an approach that balances compliance with the specification against the practical realities of their infrastructure. --- [^1]: An OAuth server (or authorization server) is the system that handles user authentication and issues access tokens. Think of it as the bouncer at a club who checks IDs and gives out wristbands. [^2]: PKCE (Proof Key for Code Exchange) is a security extension to OAuth that prevents authorization code interception attacks. It's like adding a secret handshake to the authentication process. [^3]: Dynamic Client Registration (DCR) allows applications to register themselves as OAuth clients programmatically, without manual configuration. Imagine if every app could automatically add itself to your Google account's authorized apps list - that's DCR. [^4]: Client IDs and client secrets are like username-and-password pairs for applications (not users). They identify which application is requesting access to user data. [^5]: Cloudflare Workers are serverless functions that run at the edge of Cloudflare's network. Think of them as tiny programs that can intercept and modify web requests. [^6]: Cloudflare KV is a key-value storage system. Essentially, it's a simple database for storing data like tokens and their mappings. [^7]: PKCE verification ensures that the same client that started the OAuth flow is the one completing it, preventing certain types of attacks. # What is MCP authorization? Source: https://speakeasy.com/mcp/securing-mcp-servers/authorizing-mcp-servers import { Callout } from "@/mdx/components"; Imagine giving an AI assistant the keys to your database, customer records, or financial systems. Since the Model Context Protocol (MCP) allows agentic clients to access various parts of your systems as [tools](/mcp/building-servers/protocol-reference/tools) and [resources](/mcp/building-servers/protocol-reference/resources), how do you ensure that only the right agents can access the right tools? This is where authorization comes in. Authorization determines: - **Who** can access your MCP server - **What** specific tools and resources they can use - **When** their access is valid - **How** they need to be authenticated ## What is the difference between authorization and authentication? Before diving deeper, let's clarify an important distinction: - **Authentication** verifies identity, which is to say it confirms who or what is making the request. - **Authorization** determines permissions, which is to say it decides what the authenticated entity can access. ### Why is authorization important? Suppose you have a powerful MCP server that can perform various tasks, like accessing sensitive data or executing critical operations. In this case, you need to ensure that **only authorized agents** can perform these actions. Without proper authorization, you risk an agent accessing sensitive data it's not supposed to or, even worse, [executing destructive commands](https://x.com/jxnlco/status/1910131264566485502). ### OAuth 2.1 within MCP MCP uses a subset of the [OAuth 2.1 framework](https://oauth.net/2.1/) for authorization. OAuth 2.1 is mandatory, and the MCP Specification establishes [specific requirements](https://modelcontextprotocol.io/specification/2025-03-26/basic/authorization) for implementing it in your server. 1. **Mandatory PKCE implementation**: All MCP clients must use [Proof Key for Code Exchange](https://oauth.net/2/pkce/) (PKCE) to prevent authorization code interception attacks. 2. **Metadata discovery support**: MCP servers should support [RFC8414](https://tools.ietf.org/html/rfc8414) for automatic endpoint discovery. 3. **Dynamic client registration**: MCP servers should support [RFC7591](https://tools.ietf.org/html/rfc7591) to enable seamless client onboarding. 4. **Standardized error handling**: MCP servers must respond with specific HTTP status codes (`401`, `403`, `400`) for different authorization scenarios. ### Implementing OAuth in MCP Here are a few ways you can implement OAuth when building your MCP server: 1. **Self-contained OAuth**: Your MCP server acts as both the resource provider and the authorization provider. 2. **Third-party OAuth provider**: Your MCP server integrates with established providers like Auth0. 3. **Custom OAuth provider**: You use your existing OAuth infrastructure behind your own API infrastructure to provide authorization for your MCP server. Each approach has its own trade-offs, so you'll need to choose the one that best fits your needs. ## How does MCP authorization work in real-world applications? Let's walk through an example to understand how you'd implement authorization in a real-world MCP server. ### Scenario: A document management system Suppose you've built a document management MCP server that exposes these [tools](/mcp/tools): - `createDocument` - `readDocument` - `updateDocument` - `deleteDocument` - `shareDocument` You need to make sure different users have appropriate access levels: | Role | Access level | Allowed tools | | ------ | ------------------------------------------ | ------------------------------------------------------------------------------------- | | Viewer | Can only read documents | `readDocument` | | Editor | Can read, create, and update documents | `readDocument`, `createDocument`, `updateDocument` | | Admin | Has full access to all document operations | `readDocument`, `createDocument`, `updateDocument`, `deleteDocument`, `shareDocument` | When an agent tries to access this system through an MCP client, the following flow occurs: 1. **Initial request**: An MCP client tries to call the `deleteDocument` tool without an access token. 2. **Challenge response**: The MCP server responds with a `401 Unauthorized` status. 3. **Authentication initiation**: The client redirects the agent to an authorization URL. 4. **User consent**: The client is authenticated and granted the requested permissions, for example, for an `editor` role. 5. **Token exchange**: After consent, the authorization server issues an access token. 6. **Authorized request**: The client includes this token in subsequent requests and can access only the tools allowed by the role. - For example, a client with the `editor` role can call `readDocument`, `createDocument`, and `updateDocument`, but not `deleteDocument`. 7. **Permission verification**: The MCP server validates not just the token's authenticity but also the specific permissions it grants. Here's how you can implement authorization checks in a real MCP server: ```python filename="mcp-server.py" from mcp.server import Server from mcp.auth import TokenValidator app = Server("document-manager") token_validator = TokenValidator() @app.tool("deleteDocument", description="Delete a document by ID", args={"id": str}) async def delete_document(id: str, context): # Check if user has admin role via token claims token = context.auth.token if not token: raise PermissionError("Authentication required") user_roles = token_validator.get_roles(token) if "admin" not in user_roles: raise PermissionError("Admin role required to delete documents") # Proceed with deletion if authorized result = await database.delete_document(id) return {"status": "deleted", "documentId": id} ``` In this example, the `deleteDocument` tool checks whether the user has the `admin` role before allowing the deletion of a document. If not, it raises a `PermissionError`. ## Authorization patterns There are a few authorization patterns you can implement to enhance the security of your MCP server. ### 1. Role-based access control (RBAC) Define roles (like `viewer`, `editor`, and `admin`) and assign permissions to these roles. Depending on the context, clients can then be assigned roles rather than individual permissions. ```json { "roles": { "viewer": { "permissions": ["readDocument"] }, "editor": { "permissions": ["readDocument", "createDocument", "updateDocument"] }, "admin": { "permissions": [ "readDocument", "createDocument", "updateDocument", "deleteDocument", "shareDocument" ] } } } ``` ### 2. Attribute-based access control (ABAC) Base authorization decisions on attributes (or claims) about the user, resource, action, and environment. ```python filename="mcp-server.py" @app.tool("updateDocument", args={"id": str, "content": str}) async def update_document(id: str, content: str, context): document = await database.get_document(id) # Check if user is the document owner or has editor/admin role token = context.auth.token user_id = token_validator.get_user_id(token) user_roles = token_validator.get_roles(token) if (document.owner_id == user_id or any(role in ["editor", "admin"] for role in user_roles)): # Authorized to update return await database.update_document(id, content) else: raise PermissionError("Not authorized to update this document") ``` ### 3. Third-party authorization Instead of building your own authorization system from scratch, integrate your MCP server with existing identity providers like Auth0, Okta, GitHub, or even the authentication layer of your own API. This lets you use the established identity infrastructure and avoid duplicating user management. ```mermaid sequenceDiagram participant Client as MCP Client participant Server as MCP Server participant Auth as Identity Provider Client->>Server: Connect to /sse endpoint Server-->>Client: 401 Unauthorized + auth URL Client->>Server: Redirect to /oauth/authorize Server->>Auth: Forward to provider's OAuth endpoint Auth-->>Client: Show login & consent screen Client->>Auth: User approves access Auth-->>Server: Callback with auth code Server->>Auth: Exchange code for token Auth-->>Server: Return access token Server-->>Client: Issue MCP session token Client->>Server: Call tool with session token Server->>Auth: Use provider token for API calls Auth-->>Server: Return API response Server-->>Client: Return tool response ``` In this flow: 1. The MCP server acts as a bridge between your MCP client and the identity provider. 2. The identity provider handles user authentication and consent. 3. The MCP server exchanges tokens with the provider and maintains the mapping. 4. Your tools can use the provider's APIs without exposing tokens to clients. Let's see how this works with the Sentry MCP server, which uses Sentry's OAuth service as its identity provider: ```mermaid flowchart LR Client[MCP Client] MCP[MCP Server] SentryOAuth[Sentry OAuth] SentryAPI[Sentry API] Client <-->|"Auth & Tools"| MCP MCP <-->|"OAuth Flow"| SentryOAuth Client <-->|"Consent"| SentryOAuth MCP <-->|"API Calls"| SentryAPI ``` 1. **Initial connection**: Your MCP client connects to a Sentry MCP server. 2. **OAuth initiation**: The server responds with a `401 Unauthorized`, triggering the OAuth flow. 3. **First OAuth step**: The client redirects to the MCP server's OAuth endpoint. 4. **Server-side OAuth**: The MCP server shows its own consent screen first. ![MCP server consent screen showing the client requesting access](/assets/mcp/mcp-authorization/mcp-sentry-auth.png) 5. **Provider authentication**: After approving this first step, the server redirects to Sentry's OAuth service. 6. **Provider consent**: You'll see Sentry's consent screen showing which permissions the MCP server is requesting. ![Sentry OAuth authorization dialog](/assets/mcp/mcp-authorization/mcp-sentry-approve.png) 7. **Double token exchange**: - Sentry issues an OAuth token to the MCP server (not to your client). - The MCP server creates its own session token for your client. - The Sentry token stays secure on the server side. 8. **Using tools**: When you call a tool like `list_organizations`, the following flow begins. - The server validates your MCP session token. - It uses its Sentry token to call the Sentry API. - You get the results without ever handling Sentry credentials. This setup creates a secure proxy pattern - the MCP server mediates interactions between clients and the identity provider. There are actually two OAuth flows happening: 1. One occurs between your client and the MCP server. 2. The other occurs between the MCP server and Sentry. This double-layer approach provides extra security by keeping the provider's tokens isolated from clients. For more details on how this works, see the [remote-servers](/mcp/using-mcp/remote-servers) page. ### Authorization Code Flow with PKCE For browser-based applications and most MCP clients, the specification recommends the Authorization Code Flow with PKCE (Proof Key for Code Exchange). This is for public clients that cannot keep secrets. The PKCE extension prevents authorization code interception attacks by requiring the client to generate a secret "code verifier" and its transformed value (the "code challenge"). This ensures that only the original requester can exchange the authorization code for tokens. Authorization in MCP is tightly integrated with the [transport layer](/mcp/transports). During the transport initialization handshake, the MCP server can immediately communicate its authorization requirements to the client. ## Authorization and root boundaries MCP [roots](/mcp/roots) can be used in tandem with authorization to provide an additional layer of access control. While authorization determines which actions a user can perform, roots determine **which resources are visible** to the client. For example, you might implement both in a tool like `readDocument` to make sure that a client can read only the documents within their authorized roots and only if they have the right permissions: ```python @app.tool("readDocument", args={"id": str, "rootPrefix": str}) async def read_document(id: str, rootPrefix: str, context): # First check authorization if not has_permission(context.auth.token, "documents:read"): raise PermissionError("No permission to read documents") # Then validate against roots if not context.roots.can_access(f"{rootPrefix}/{id}"): raise PermissionError("Document outside accessible roots") # Return the document only if both checks pass return get_document(id) ``` ## Best practices for secure MCP authorization Here are a few best practices to keep in mind when implementing authorization in your MCP server: 1. **Apply the principle of least privilege**: Grant only the permissions necessary for the intended function. 2. **Implement short-lived tokens**: Set reasonable expiration times for access tokens. 3. **Validate tokens properly**: Check signature, expiration, issuer, and audience claims. 4. **Provide clear permission errors**: Help users understand why access was denied. 5. **Use TLS for all communications**: Encrypt all authorization-related traffic. 6. **Implement rate limiting**: Protect against brute-force and denial-of-service attacks. # Design MCP tools Source: https://speakeasy.com/mcp/tool-design After building, successfully deploying, and monitoring your MCP server, the natural next step is to optimize it. You can unlock substantial performance gains by focusing on strategic tool curation, selecting, refining, and organizing tools to reduce overhead and maximize server efficiency. This guide presents several tool curation principles for MCP server optimization and demonstrates how to use the curation features in [Gram](https://getgram.ai). ## What is tool curation? Tool curation involves choosing and organizing MCP tools to expose to LLM models. By carefully selecting, naming, and describing your tools for LLMs, you can manage context windows and prevent hallucinations. Tool curation combines technical optimization with prompt engineering and significantly impacts how well your MCP server performs in production. Tool curation matters because: - **LLMs break down with too many tools:** LLMs become unreliable when exposed to more than 30-40 tools. Beyond this threshold, they display tool hallucination (inventing non-existent tools) and perform irrelevant tool selections (using the wrong tools for tasks). These failed attempts and incorrect tool calls increase computational overhead and API costs. - **Tool descriptions have a greater impact than model choice:** Optimizing your tool descriptions and names can have a [bigger impact on the quality of your MCP server](https://www.speakeasy.com/blog/cost-aware-pass-rate) than the underlying LLM models your users choose. A well-curated toolset with clear descriptions will outperform a comprehensive but poorly organized toolset, even when using smaller, cheaper models. - **Every tool name and description becomes part of the prompt that guides LLM decision-making:** Tool curation works like prompt engineering for tools. Similar to crafting effective prompts, tool curation requires understanding how LLMs process information and make choices. ### When do you need tool curation? Just like premature optimization in software, curating tools too early can be a waste of effort if you don't yet understand real user behavior. Instead, wait until clear warning signs emerge that indicate tool curation is needed, such as: - **Tool hallucination:** The LLM invents non-existent tools. - **Irrelevant tool selection:** The LLM uses tools that aren't suited to the current goal. - **Increased end-user costs:** MCP clients make many failed attempts and incorrect tool calls. - **Agent confusion:** Tasks that should be simple require multiple attempts. - **Context window exhaustion:** Tool definitions consume too much available context. ## How to do tool curation effectively? The best place to begin tool curation depends on how you built your MCP server. If you generated your MCP server **from an API** (using a tool like FastMCP), start by editing your code directly. For example, you can add detailed docstrings or update function metadata. If you generated your MCP server **from an OpenAPI document** (using a tool like [Speakeasy](https://www.speakeasy.com/mcp)), rather start by editing the OpenAPI document itself. Use the following principles to edit OpenAPI documents for effective tool curation: ### Group tools by workflow, not API structure **❌ Resource-based grouping:** API documentation often organizes endpoints by resource type, because it makes sense for developers reading technical documentation. ```txt User Management Tools: - create_user - update_user - delete_user - get_user_by_id - list_all_users Ticket Management Tools: - create_ticket - update_ticket - delete_ticket - get_ticket_by_id - list_all_tickets ``` However, organizing API endpoints by resource type forces agents to jump between tool categories to complete simple workflows. For example, an agent trying to create a support ticket for a customer must search through the `User Management Tools`, then switch to the `Ticket Management Tools`, potentially missing the connection between related operations. **✅ Workflow-based grouping:** Because agents think in terms of tasks and workflows, rather organize the endpoints according to workflows. This lets agents complete entire tasks without switching contexts or hunting through unrelated tool categories. Each toolset represents a specific workflow. ```txt Customer Support Workflow: - search_customer (find who needs help) - get_customer_tickets (see their history) - create_support_ticket (log the issue) - update_ticket_status (track progress) License Management Workflow: - lookup_customer_licenses (check current status) - validate_license_key (verify authenticity) - create_new_license (issue replacement) - send_license_notification (inform customer) ``` ### Include dependency tools in each toolset **❌ Missing dependencies:** Many tools require information from other tools to function properly. If you indicate the required information, most developers know how to find it. ```txt Quick License Creation Toolset: - create_license (requires user_id, product_id, license_type) ``` However, when you exclude prerequisite tools from a toolset, agents can get stuck with incomplete information and fail to complete workflows. For example, if an agent trying to create a license doesn't know how to find the required `user_id` or determine valid `license_type` values, the workflow breaks down immediately. **✅ Complete dependency chain:** Rather create a toolset that supports the complete workflow, from discovery through validation, ensuring agents have all the information they need to succeed. ```txt License Management Toolset: - search_customers (get user_id by name/email) - list_products (see available product_id values) - get_license_types (understand valid license_type options) - create_license (complete the action with all required data) - validate_license (verify the creation worked) ``` ### Use clear, consistent naming patterns **❌ Mixed conventions:** Using varying punctuation and synonyms doesn't impair most developers' understanding of tools. ```txt Customer Tools: - searchCustomers - find_user_by_id - getUserDetails - lookup_customer_info - fetch_user_data ``` However, inconsistent tool names create cognitive overhead for agents. When agents learn patterns, they expect consistency. Mixed conventions force agents to memorize individual tool names rather than predicting them. For example, the above tools all retrieve customer information but the use of `camelCase`, `snake_case`, and different verbs makes it harder for LLMs to discover and remember them. **✅ Consistent patterns:** Rather use predictable naming patterns for related tools. For example, the consistent `search_*` and `get_*` patterns help agents predict tool names and understand the distinction between searching (finding multiple results) and getting (retrieving specific items) information. ```txt Customer Tools: - search_customers - get_customer_by_id - get_customer_details - get_customer_tickets - get_customer_billing ``` ### Write descriptions for agents, not humans **❌ Technical descriptions:** The descriptions in API documentation usually target developers who understand technical implementation details. ```txt POST /api/v1/tickets Description: "Creates a new ticket resource in the database with the provided JSON payload. Returns a 201 status code with the created ticket object including auto-generated ID and timestamp fields." ``` However, agents need less technical information and instead require task-oriented descriptions that explain when and how to use tools within larger workflows. The example above describes the technical implementation but provides no guidance about when and how to use the tool. **✅ Task-oriented descriptions:** Rewrite the description for agents by specifying the scenarios when they should use the tool and any preparatory tasks they first need to perform. For example, the following description tells agents exactly when to use the tool, what preparation is needed, and how it fits into customer service workflows. ```txt create_support_ticket Description: "Creates a new customer support ticket when a customer reports an issue. Use this when customers need help with technical problems, billing questions, or feature requests. Requires customer_id (use search_customers first) and issue description." ``` ### Provide workflow context in descriptions **❌ Isolated descriptions:** Developers know that tools rarely exist in isolation, and intuitively understand the larger workflows they form a part of based on their descriptions. ```bash create_license: "Creates a new license." search_customers: "Finds customers in the database." ``` However, these descriptions treat each tool as independent, providing no guidance about workflow relationships or sequencing. **✅ Workflow-aware descriptions:** Add workflow context to descriptions to help agents understand how tools connect and which actions come before or after each tool call. For example, the following descriptions help agents understand tool relationships and natural workflow sequences. ```txt create_license: "Creates a new license for a customer. Use after search_customers to get the customer_id, and list_products to choose the right product_id. Always validate the created license afterward." search_customers: "Finds customers by name, email, or company. Use this first step before creating tickets, licenses, or accessing customer data. Returns customer_id needed for other customer operations." ``` ### Avoid the "everything" toolset **❌ The giga-toolset:** You may be tempted to save time by including every available tool in a single toolset. ```txt Complete Business Management: - 47 customer management tools - 23 inventory tools - 31 financial tools - 18 reporting tools - 12 user administration tools - 8 system configuration tools ``` However, this overwhelming collection forces agents to process irrelevant options for every task, slowing down decision-making and increasing error rates. One way you can prevent this decision paralysis is by organizing your toolsets according to user personas. For example, a customer service agent might only need the tools for listing tickets, listing customers, viewing customers, and adding internal notes. **✅ Focused toolsets:** Instead of exposing the agent to all the tools in your MCP server, create a focused toolset to prevent it from spending more time analyzing options than completing actual work. In the following example, each toolset has a clear purpose and contains only the tools relevant for that purpose, enabling faster agent decision-making with fewer errors. ```txt Customer Service: - search_customers - get_customer_tickets - create_ticket - update_ticket_status - add_ticket_note Emergency Escalation: - search_priority_tickets - escalate_to_manager - notify_technical_team - create_incident_report ``` ## Tool curation in Gram [Gram](https://getgram.ai) is a platform built by Speakeasy that helps you create, curate, and host MCP servers. You upload your OpenAPI document, then the platform generates tools based on your endpoints and allows you to curate them into a toolset. Gram supports tool curation by enabling you to: - Combine just the tools you need into streamlined toolsets – even from multiple sources - Edit tool names and descriptions in the UI to provide improved context for LLMs ### Taskmaster Demo To demonstrate tool curation in Gram, we'll use the [Taskmaster Internal API](https://github.com/speakeasy-api/examples/tree/main/taskmaster-internal-api). When you upload the OpenAPI document, `openapi.yaml` from the **Taskmaster Internal API** project, the default toolset includes tools from different domains: **Customer-Service**, **Marketing**, and **Licenses**. ![Displaying Taskmaster toolset](/assets/mcp/optimizing-your-mcp-server/taskmaster-default-toolset.png) If you use the Taskmaster MCP server as is for a customer service agent (without curating the tools), you can deploy and install it in your MCP client without issue. However, you encounter problems when prompting your MCP client, for example, to create a user. ![Claude asking for confirmation](/assets/mcp/optimizing-your-mcp-server/claude-asking-confirmation.png) Claude notices the different possibilities and asks for confirmation. This may work well for interactive use, but automatic agents either stop the thought process or use the tool that seems most adequate, increasing the risk of making an error. Instead, you can reorganize the Taskmaster MCP server by creating a specific workflow-optimized toolset. In this case, you should enable only the tools necessary for a customer service workflow that allows a customer service agent to: - List tickets - Create and validate licenses - Retrieve user information ![Selecting workflow tools](/assets/mcp/optimizing-your-mcp-server/selecting-workflow-tools.png) This focused toolset reduces tool confusion and improves agent decision-making. ![New curated toolset](/assets/mcp/optimizing-your-mcp-server/curated-customer-service-toolset.png) After organizing the toolset, you can edit individual names and descriptions to provide more clarity on what each tool does. Hovering over a tool name or description reveals an **Edit** button to allow you to update the relevant information. ![Tool edit hover state](/assets/mcp/optimizing-your-mcp-server/tool-edit-hover-state.png) Click **Edit** to open the editing modal and enter more descriptive values based on the tool curation principles from the previous section. ![Edit tool description modal](/assets/mcp/optimizing-your-mcp-server/edit-tool-description-modal.png) ## Conclusion In this guide, you explored how to optimize your MCP server through tool curation. By narrowing your toolsets to only the essential tools, you not only improve the experience for both users and agents but also lower costs — since even smaller models with shorter context windows can perform effectively with a focused set of tools. MCP server optimization extends beyond tool curation. You can [monitor](/mcp/monitoring-mcp-servers) your MCP server to identify other optimization opportunities through data analysis. # app/main.py Source: https://speakeasy.com/mcp/tool-design/designing-rag-tools-for-llms Retrieval-Augmented Generation (RAG) and the Model Context Protocol (MCP) are often positioned as alternatives — with RAG enabling semantic searches and MCP allowing API actions — but they can be complementary. You can use RAG to search your knowledge base efficiently and MCP to standardize how LLMs access that search. This guide shows you how to design RAG tools specifically for LLMs. It demonstrates the input patterns that work, the output structures LLMs need, and the best design choices for RAG tools. ## RAG overview RAG is an architecture pattern for semantic search. It combines information retrieval with text generation, allowing LLMs to search external databases or sources for relevant context before generating an answer. This usually works by breaking documents into chunks, converting those chunks into vectors, storing them in a database, and then retrieving information based on the semantic similarity to user queries. ## Why MCP servers should have a RAG tool MCP servers provide tools that LLMs interact with to perform actions such as searching databases, calling APIs, and updating records. RAG provides LLMs with additional context by semantically searching the knowledge base. MCP gives LLMs capabilities by connecting them to a system. For example, a RAG tool could enable your enterprise AI chatbot to answer questions from your user guides and documentation, and MCP tools could help customer support agents retrieve a [user's license information](/mcp/using-mcp/use-cases/customer-support) or create a new support ticket. In this example, RAG handles knowledge retrieval and MCP handles your system actions. ### The problem with MCP resources MCP servers provide three primitives: tools, resources, and prompts. [MCP resources](/mcp/core-concepts/resources) are designed to give context to LLMs. These resources can be images, guides, or PDFs. MCP resources seem like the natural choice for searching documentation — you expose your docs, and the LLM accesses them. But the problem is scale. MCP resources dump the entire collection or document into the context window with no processing. If MCP dumps a 100-page product guide in the LLM context, it risks bloating the context and immediately hitting context limits, which could cause timeouts, refusals, or hallucinations. Most LLM clients, including Claude Desktop and ChatGPT, don't index resources from MCP servers due to rate limits and context window issues. In our [RAG vs MCP](/blog/rag-vs-mcp) blog, we compared an RAG implementation to an MCP implementation for searching Django documentation. RAG used 12,405 tokens and found the answer in 7.64 seconds. MCP used more than double that number of tokens (30,044) and took over four times longer (33.28 seconds) than RAG, but still failed to find the answer because the relevant content fell beyond the first 50 pages it could fit in the context window. ### How RAG tools solve context bloating This is where RAG tools come in handy. Instead of an LLM loading, managing, and searching multiple MCP resources, it can call a RAG tool with a natural language query. The tool handles embedding, vector search, and relevance filtering, and returns only the chunks most relevant to the search. The LLM gets precisely what it needs without managing the search infrastructure. RAG tools also enable features that don't work with static resources, including: - **Relevance scoring:** LLMs can request more context when scores are low. - **Metadata filtering:** LLMs can search for specific versions or sections of a resource. - **Context management:** You can implement automatic token budgeting. The following diagram illustrates how this architecture works in practice: ![RAG tool architecture: User queries Claude Desktop, which calls Gram's MCP server, which calls your FastAPI, which queries the RAG service and ChromaDB](/assets/mcp/tool-design/designing-rag-tools-for-llms/illustration.png) ## RAG input parameters A well-designed RAG tool needs three types of parameters: the search query itself, result controls, and quality filters. If an LLM uses incorrect parameters, it could fail to express what it needs or be flooded with irrelevant results. ### The query parameter The query parameter should actually be a natural language query, not a list of keywords, because the RAG system uses embeddings for semantic search, and the embedding models (`all-MiniLM-L6-v2` or `text-embedding-3-small` for OpenAI) are trained on natural language sentences, not keyword lists. When a user asks, *"How do I work with curved geometries in Django's GIS module?"* the LLM immediately parses the intent (implementation guidance), identifies the domain (Django GIS geometry handling), and understands the context (a how-to question). Forcing the LLM to translate the natural language prompt into structured keywords like `["django", "gis", "curve"]` with filters like `{"type": "tutorial"}` throws away semantic understanding. The LLM would have to decide which words were keywords and which were context, map natural language to your filter taxonomy, and lose the semantic relationships that make embeddings work. This would give you worse search results and waste tokens. ### The result count control LLMs understand and manage their context windows. In the tool parameters, let the LLM specify how many results it needs. Cap results at `10` to prevent context overflow. Make this parameter optional with a documented default (a default of `3` results works well). ### Quality filtering Not all search results are equally relevant, so you should allow the LLM to filter by quality. For example, when you query a vector database like [ChromaDB](https://docs.trychroma.com/docs/overview/getting-started?lang=typescript#next-steps), it can (if configured) return results ranked by cosine similarity, a score measuring how close the query embedding is to each document embedding. A score of `1.0` means the query and embedding have identical semantic meanings, `0.5` means they are somewhat related, and `0.0` means they are unrelated. This keeps low-quality results out of the LLM's context window entirely. When Claude asks for `min_score=0.7`, the RAG tool enforces this at retrieval time and filters out anything below that threshold. The LLM uses these scores to adjust its strategy. If it receives two results with scores of `0.72` and `0.71`, it knows the match is marginal, and it may lower the threshold to `min_score=0.6` for a broader search. If it gets ten results, all above `0.9`, it knows the search is highly targeted. ## How to design a RAG tool If you're exposing RAG capabilities via multiple endpoints, rather use a single endpoint. When you have numerous guides or documentation sets to index, you may be tempted to use separate tools or endpoints, but if you're designing RAG for an enterprise with dozens of products and documentation sets, exposing too many tools to the LLM could result in a tool explosion and cause context bloating. The LLM may face decision paralysis, leading to incorrect tool choices or hallucinations. Instead, use a single search tool with a `collection` parameter for specifying which documentation set it should search (for example, `collection="user-guide"` or `collection="api-reference"`). ### Response format LLMs need results in a format they can immediately use, such as the following: ```json { "results": [ { "content": "The actual documentation text...", "source": "https://docs.djangoproject.com/en/5.2/ref/contrib/gis/", "score": 0.87 }, { "content": "More documentation text...", "source": "https://docs.djangoproject.com/en/5.2/releases/5.2/", "score": 0.82 } ], "total_found": 2, "tokens_estimate": 1847 } ``` The response format will vary depending on your case, but you should follow these best practices: - **Use flat results arrays:** Don't nest results in complex structures because the LLM iterates through them sequentially. - **Return content first:** Put the actual text in `content`, not `text`, `document`, or `chunk`. - **Include sources:** The LLM needs to cite its sources. URLs, page numbers, or document IDs work. - **Expose scores:** Let the LLM judge result quality. If all scores are below `0.6`, it knows the search was weak and might rephrase the query. - **Provide token estimates:** This is critical for context management. The LLM needs to determine whether it can fit these results, along with its reasoning, in the context window. Divide the total number of characters by four for a rough estimate (this works well for English documentation). - **Avoid returning too much data to the LLM:** ```json // ❌ Bad: too much metadata { "results": [ { "content": "...", "metadata": { "chunk_id": "abc123", "embedding_model": "all-MiniLM-L6-v2", "embedding_dimensions": 384, "created_at": "2025-01-15T10:23:45Z", "database_shard": "shard-3", "index_version": "v2.1" } } ] } ``` ### Error responses for RAG tools When searches fail, LLMs need actionable errors. Compare the following versions of an error: ```json // ❌ Bad: Generic error { "error": "Search failed", "code": 400 } // ✅ Good: Actionable error { "error": "no_results_found", "message": "No documentation found for 'Djago GIS features'", "attempted_query": "Djago GIS features" } ``` The second version tells the LLM what went wrong (a typo in "Django") and echoes the query so the LLM can verify the search. ## How to build a Django documentation RAG MCP server Let's build a Django documentation search API and expose it as an MCP tool through Gram. This example extends the RAG implementation from the RAG vs MCP post by wrapping it in a REST API with the correct input/output design for LLM consumption. You can find the complete project in the [Speakeasy Examples repository](https://github.com/speakeasy-api/examples/tree/main/rag-mcp-example), in the `complete` directory. Clone the project and use the code in the `base` folder to follow the instructions below. ### Set up the project Clone and install the dependencies: ```bash git clone https://github.com/speakeasy-api/examples.git cd examples/rag-mcp-example/base uv sync ``` Download the [Django 5.2.8 documentation PDF](https://app.readthedocs.org/projects/django/downloads/pdf/5.2.x/) and save it in the `base` directory as `django.pdf`. Run the indexing script to build the ChromaDB collection: ```bash uv run python scripts/build_rag_index.py ``` ### Define the search interface First, define the schemas in the `app/main.py` file: ```python import logging from typing import List, Optional from pathlib import Path from pydantic import BaseModel, Field from fastapi import FastAPI from fastapi.openapi.utils import get_openapi from sentence_transformers import SentenceTransformer import chromadb # Configure logging logging.basicConfig(level=logging.INFO) logger = logging.getLogger(__name__) # Configuration CHROMA_PATH = "./chroma_db" CHROMA_COLLECTION = "django_docs" DEFAULT_MAX_RESULTS = 3 MAX_ALLOWED_RESULTS = 10 DEFAULT_MIN_SCORE = 0.5 # Models class SearchRequest(BaseModel): query: str = Field(..., description="Natural language search query", example="What's new in django.contrib.gis?") max_results: Optional[int] = Field(default=3, ge=1, le=10, description="Maximum number of results") min_score: Optional[float] = Field(default=0.5, ge=0.0, le=1.0, description="Minimum relevance score") class SearchResult(BaseModel): content: str = Field(..., description="The documentation chunk") source: str = Field(..., description="Source reference") score: float = Field(..., description="Relevance score (0-1)") class SearchResponse(BaseModel): results: List[SearchResult] total_found: int tokens_estimate: int ``` The query accepts natural language directly. The `max_results` attribute, capped at `10`, prevents context overflow, and the `min_score` defaults to `0.5` for inclusive results, allowing the LLM to raise the threshold when it needs higher confidence. The `SearchResponse` response schema keeps results in a flat array for easy LLM iteration. The score field lets the LLM judge quality and adjust queries. The `tokens_estimate` attribute helps with context window management, critical for preventing overflow. > **Note:** > Token estimation divides the total number of characters by four, because most tokenizers average about four characters per token in English. ### Build the RAG search logic The `RAGService` class handles the vector search: ```python # app/main.py class RAGService: def __init__(self): self.model = SentenceTransformer("all-MiniLM-L6-v2") self.client = chromadb.PersistentClient(path=CHROMA_PATH) self.collection = self.client.get_collection(CHROMA_COLLECTION) def search(self, query: str, max_results: int, min_score: float): # Generate query embedding query_embedding = self.model.encode(query).tolist() # Query ChromaDB search_results = self.collection.query( query_embeddings=[query_embedding], n_results=min(max_results * 3, 50) ) # Convert results documents = search_results["documents"][0] distances = search_results["distances"][0] ids = search_results["ids"][0] results = [] for doc, distance, doc_id in zip(documents, distances, ids): score = 1.0 / (1.0 + distance) if score >= min_score: results.append(SearchResult( content=doc, source=doc_id, score=round(score, 3) )) # Sort by score and limit results.sort(key=lambda x: x.score, reverse=True) total_found = len(results) filtered_results = results[:max_results] # Estimate tokens (rough: 4 chars = 1 token) total_chars = sum(len(result.content) for result in filtered_results) tokens_estimate = total_chars // 4 return filtered_results, total_found, tokens_estimate ``` The service retrieves `max_results * 3` candidates to ensure enough candidates survive the score filtering. ChromaDB returns distances, which are converted to 0-1 similarity scores using `1 / (1 + distance)`. The results are filtered by `min_score`, sorted by score descending, and limited to `max_results`. ### Wire up the search API The FastAPI `/search` endpoint wires everything together: ```python # app/main.py app = FastAPI( title="Django Documentation RAG API", description="Semantic search over Django 5.2.8 documentation using RAG (Retrieval-Augmented Generation)", version="1.0.0", openapi_tags=[ { "name": "search", "description": "Semantic search operations over Django documentation", }, ], ) rag_service = RAGService() @app.post( "/search", response_model=SearchResponse, tags=["search"], summary="Search Django documentation", operation_id="search_documentation", description=""" Perform semantic search over Django 5.2.8 documentation chunks. Returns relevant documentation sections with similarity scores and token estimates. """, responses={ 200: {"description": "Successful search with results"}, 422: {"description": "Validation error"}, }, ) async def search_documentation(request: SearchRequest): """Search Django documentation using semantic similarity""" results, total_found, tokens_estimate = rag_service.search( query=request.query, max_results=request.max_results or DEFAULT_MAX_RESULTS, min_score=request.min_score or DEFAULT_MIN_SCORE ) return SearchResponse( results=results, total_found=total_found, tokens_estimate=tokens_estimate ) ``` The `operation_id="search_documentation"` becomes the MCP tool name that Claude will call. The description tells the LLM what this tool does and when to use it. FastAPI handles validation and serialization automatically. ### Customize the OpenAPI document The MCP server uses an OpenAPI document that we'll host on Gram. Gram provides an OpenAPI [extension](/docs/gram/build-mcp/advanced-tool-curation#provide-rich-context) `x-gram` that helps LLMs better understand the tools they call. To customize the OpenAPI document, create a function to rewrite the attributes you want: ```python # app/main.py def custom_openapi(): """Customize OpenAPI Output with x-gram extensions for getgram MCP servers""" if app.openapi_schema: return app.openapi_schema openapi_schema = get_openapi( title=app.title, version=app.version, description=app.description, routes=app.routes, tags=app.openapi_tags, ) # Add x-gram extensions to specific operations x_gram_extensions = { "search_documentation": { "x-gram": { "name": "search_django_docs", "summary": "Search Django documentation using semantic similarity", "description": """ This tool performs semantic search over Django 5.2.8 documentation using RAG (Retrieval-Augmented Generation). It returns relevant documentation chunks with similarity scores and token estimates for LLM context management. Perfect for finding specific Django functionality, code examples, and best practices. - Query should be natural language describing what you're looking for - Results are ranked by semantic similarity (score 0-1, higher is better) - Token estimates help manage LLM context windows - Supports filtering by minimum relevance score and maximum result count """, "responseFilterType": "jq", } }, } # Apply x-gram extensions to paths if "paths" in openapi_schema: for path, path_item in openapi_schema["paths"].items(): for method, operation in path_item.items(): if method.lower() in ["get", "post", "put", "delete", "patch"]: operation_id = operation.get("operationId") if operation_id in x_gram_extensions: operation.update(x_gram_extensions[operation_id]) app.openapi_schema = openapi_schema return app.openapi_schema # Override the default OpenAPI function app.openapi = custom_openapi ``` ### Run the server Add the following lines at the end of the `app/main.py` file to run the server: ```python if __name__ == "__main__": import uvicorn uvicorn.run("app.main:app", host="0.0.0.0", port=8000, reload=True) ``` Start the server with the following command: ```bash uv run uvicorn app.main:app --host 0.0.0.0 --port 8000 --reload ``` ### Deploy the MCP server with Gram Gram is a service that lets you generate MCP servers from OpenAPI documents. You build a standard REST API, provide the OpenAPI document, and Gram handles the MCP protocol implementation, hosting, and authentication. This means you focus on implementing your endpoints and business logic – whether that's RAG search, database queries, or API operations – rather than coding the MCP server and managing the infrastructure. Coding and building an MCP server from scratch is doable. For example, you can use tools like FastMCP for development and FastMCP Cloud for hosting the server, and use MCP SDKs to build MCP servers and expose them via the Streamable HTTP transport. However, you still need to manage the infrastructure (maintaining and monitoring a service), implement CI/CD pipelines, and handle the security. MCP requires OAuth 2.1 for authentication, which adds complexity. With Gram, you can upload the OpenAPI document from the cloned project, configure the API URL using an [ngrok](https://ngrok.com/) forwarding link, create the toolsets, enable remote MCP distribution, and then install and test it in Claude Desktop. [Sign up for Gram](https://getgram.ai) and follow these steps: - On the [**Toolsets** page](https://docs.getgram.ai/build-mcp/create-default-toolset), click **Get Started** and upload the RAG API OpenAPI document, `rag-mcp-example/base/openapi.yaml`. - Create a toolset named `Docs-Api-Rag` and add the `search_django_docs` tool. ![Gram toolset creation](/assets/mcp/tool-design/designing-rag-tools-for-llms/gram-toolset-creation.png) - Click on the **Docs-Api-Rag** toolset to open it, navigate to the [**Auth** tab](https://docs.getgram.ai/concepts/environments), and set `DOCS_API_SERVER_URL` to the URL of your tool's API. If you're following this guide with the local RAG MCP API, expose the API with [ngrok](https://ngrok.com/) by running the `ngrok http 127.0.0.1:8000` command and use the forwarding URL to fill in the `DOCS_API_SERVER_URL` variable. - In **Settings**, create a [Gram API key](https://docs.getgram.ai/concepts/api-keys). ### Connect to Claude Desktop In your **Docs-Api-Rag** toolset's **MCP** tab, enable the MCP server by clicking **Enable** and then clicking **Enable Server** in the modal that opens. Scroll to the **Visibility** section and set the server visibility to public. Under the **MCP Installation** section, click the **View** button to be redirected to the MCP installation page details. Copy the raw configuration details. ![Gram raw configuration](/assets/mcp/tool-design/designing-rag-tools-for-llms/gram-raw-configuration.png) Open Claude Desktop, navigate to **Settings -> Developer**, and click **Edit Config**. ![Claude Desktop edit config](/assets/mcp/tool-design/designing-rag-tools-for-llms/claude-desktop-edit-config.png) Claude will redirect you to its configuration file. Open `claude_desktop_config.json` and add the raw configuration you copied from Gram to the file contents: ```json { "mcpServers": { "DocsRagServer": { "command": "npx", "args": [ "mcp-remote", "https://app.getgram.ai/mcp/rxxxx", "--header", "Gram-Environment:${GRAM_ENVIRONMENT}", "--header", "Authorization:${GRAM_KEY}" ], "env": { "GRAM_ENVIRONMENT": "default", "GRAM_KEY": "gram_live_xxxxxxx" } } } } ``` Replace the value of `GRAM_ENVIRONMENT` with `default` or the name of the environment where you store the environment variables, and replace the value of `GRAM_KEY` with your Gram key. Save the configuration and relaunch Claude Desktop. ### Test with Claude To test the RAG tool, open Claude Desktop and send the following prompt: ```txt Hi Claude. What's new in Django 5.2, mostly Django GIS? Are curved geometries supported? ``` Claude will first use the MCP Rag tool to conduct a semantic search, then reply. ![Claude RAG search result](/assets/mcp/tool-design/designing-rag-tools-for-llms/claude-rag-search-result.png) Disable both the RAG tool and Claude's web search feature, then ask the same question. Claude will indicate uncertainty about Django 5.2 GIS features because the information is beyond its January 2025 training cutoff, and it has no way to retrieve current documentation. ![Claude knowledge cutoff response](/assets/mcp/tool-design/designing-rag-tools-for-llms/claude-knowledge-cutoff-response.png) ## Further exploration Now that you've built a RAG tool for searching documentation, consider what else becomes possible when you combine RAG with MCP tools. - **Design a [customer support agent](/mcp/using-mcp/use-cases/customer-support):** Combine a RAG tool for your product documentation with the [Zendesk MCP server](/mcp/using-mcp/mcp-server-providers/zendesk) (or another MCP server with CRM tools, support tickets, and analytics). The agent learns product context from your documentation and then pulls customer data to provide personalized support responses. - **Power a developer code assistant:** Build a RAG tool for your SDK documentation and code examples, and pair it with MCP tools that interact with your sandbox API. The LLM searches for implementation patterns, retrieves example code, and tests it against your sandbox environment. - **Build an [account management](/mcp/using-mcp/use-cases/account-management) assistant for your sales team:** Create a RAG tool that searches your company's sales playbooks and account management guides, and pair it with the [HubSpot MCP](/mcp/using-mcp/mcp-server-providers/hubspot) server. When a sales agent asks the assistant to *"update this client's status to renewal stage and log our last conversation,"* the LLM uses RAG to check your renewal protocols, then updates the contact record and creates the activity log in your CRM following those guidelines. ## Final thoughts RAG and MCP are often depicted as competing approaches, but they're most powerful when used together. An AI agent might use a RAG tool to search your product documentation for implementation guidance, then use other MCP tools to create tickets, update records, or query live data. This combination gives agents both knowledge and agency. If you're building RAG tools for MCP, check out existing implementations like [mcp-crawl4ai-rag](https://github.com/coleam00/mcp-crawl4ai-rag) and [rag-memory-mcp](https://github.com/ttommyth/rag-memory-mcp) for more patterns. To host and manage your MCP servers using Gram, explore [Gram's documentation](https://docs.getgram.ai). # Dynamic tool discovery in MCP Source: https://speakeasy.com/mcp/tool-design/dynamic-tool-discovery Say your WhatsApp MCP server's WhatsApp authentication expires, and you need to re-authenticate. We want to hide the `getWhatsAppChatById` tool until the user re-authenticates. MCP allows you to dynamically change the list of available tools at runtime. When the MCP server detects that the authentication has expired, it can send a `notifications/tools/list_changed` message to notify the MCP client that the list of tools has changed. The MCP client can then call `tools/list` to get the updated list of tools. MCP allows you to send a `notifications/tools/list_changed` message **from the MCP server to the MCP client** to notify it that the list of available tools has changed. This is useful for dynamic tool discovery, where the MCP server can add or remove tools at runtime. ```json { "method": "notifications/tools/list_changed" } ``` The MCP client can then call `tools/list` to get the updated list of tools. This allows the MCP server to dynamically change its capabilities without requiring the MCP client to reconnect or reinitialize. Here's what that would look like in our WhatsApp MCP server: ```typescript // When the MCP server detects that the authentication has expired, it can send a notification const authenticationExpiredListener = () => { getWhatsAppChatById.disable(); // Disable the tool }; ``` That's it! The TypeScript SDK handles the `notifications/tools/list_changed` message for you, so you don't need to worry about the details. You just call `disable()` on the tool, and the LLM client will automatically update its list of available tools. When the user re-authenticates, you can re-enable the tool by calling `enable()`: ```typescript const reAuthenticatedListener = () => { getWhatsAppChatById.enable(); // Re-enable the tool }; ``` ## Use cases for dynamic tool discovery Dynamic tool discovery is particularly useful in scenarios where: - **Authentication state changes**: Hide tools that require authentication when credentials expire - **Feature availability varies**: Show different tools based on user permissions or subscription tiers - **Resource availability changes**: Hide tools that depend on external services when those services are unavailable - **Context-dependent tools**: Show tools that are only relevant in certain contexts (e.g., only show file operations when a project is open) By dynamically adjusting the available tools, you can provide a cleaner, more intuitive experience for users without cluttering their tool list with unavailable options. # Generating MCP tools from OpenAPI: benefits, limits and best practices Source: https://speakeasy.com/mcp/tool-design/generate-mcp-tools-from-openapi AI agents are becoming a standard part of software interaction. From Claude Desktop assisting with research to custom agents automating workflows, these tools need a reliable way to interact with external data. This is where the [Model Context Protocol (MCP)](https://modelcontextprotocol.io/) comes in. Introduced by Anthropic in November 2024, MCP provides a universal standard for connecting AI agents to APIs and data sources. Think of it as the bridge between natural language and your API endpoints. If you already have an API documented with OpenAPI, you're in luck. An ecosystem of tooling has sprung up that can automatically generate a functional MCP server from your existing OpenAPI document. However there are limits to what's possible and open questions about what good practice looks like. In this guide, we'll explore how to optimize your OpenAPI document for MCP server generation and look at tools that make this possible. ## From OpenAPI to MCP: Where generators fit in Now, why does this matter if you already have an OpenAPI document? Because, conceptually, OpenAPI specs already contain everything needed to create API-based MCP tools: - **Endpoint paths and methods** define what operations are available - **Parameters and request schemas** specify what inputs each operation needs - **Response schemas** describe what data comes back - **Operation descriptions** explain the purpose of each endpoint An MCP generator transforms your OpenAPI specification into a functioning MCP server that exposes your API endpoints as tools AI agents can use: ![Generation explained](/assets/blog/generate-mcp-from-openapi/generation-explained.png) Your OpenAPI document serves as the source of truth, with its operations documented in OpenAPI format. An MCP generator reads this document and automatically creates tool definitions for each endpoint. The generated MCP server then acts as a bridge, translating AI agent requests into proper API calls. AI agents can discover and use your API's capabilities through the MCP server, without needing to understand your API's specific implementation details. Your OpenAPI document becomes the single source of truth, eliminating the need to manually maintain two separate specifications. Your MCP server stays in sync with your API automatically. ## Tools for generating MCP servers from OpenAPI documents Three platforms and tools automatically generate MCP servers from OpenAPI documents: - **[Gram](https://getgram.ai)** is a managed platform made by Speakeasy that provides instant hosting and built-in toolset curation. - **[FastMCP](https://github.com/jlowin/fastmcp)** is a Pythonic framework that converts OpenAPI specs and FastAPI applications into MCP servers with minimal code. - **[openapi-mcp-generator](https://github.com/harsha-iiiv/openapi-mcp-generator)** is an open-source CLI tool that generates standalone TypeScript MCP servers. These tools differ primarily in their hosting models. **Managed platforms** like Gram and FastMCP Cloud handle hosting and infrastructure for you - on Gram you upload your OpenAPI document and you get an instantly accessible MCP server. **Self-hosted** tools like openapi-mcp-generator generate code that you deploy and run yourself, giving you full control over infrastructure, customization, and deployment. Here's how they compare across hosting model and automation level: ![Comparing MCP server generators](/assets/blog/generate-mcp-from-openapi/comparing-mcp-server-generators.png) **[Gram](https://getgram.ai)** offers the fastest path to production with a fully managed platform - no infrastructure to maintain. **[FastMCP](https://github.com/jlowin/fastmcp)** gives you both options: use the Python framework for self-hosted servers or FastMCP Cloud for managed hosting. **[openapi-mcp-generator](https://github.com/harsha-iiiv/openapi-mcp-generator)** generates standalone TypeScript servers for complete self-hosted control. ### MCP server generators comparison Here's a more detailed comparison of the features of each: | Feature | Gram | FastMCP | openapi-mcp-generator | | -------------------- | ------------- | ------------------- | --------------------- | | **Language** | N/A (hosted) | Python | TypeScript | | **Setup complexity** | Low | Low | Low | | **Customization** | Config-based | Programmatic | None | | **Tool curation** | Yes, built-in | Programmatic | None | | **Hosting** | Managed | Self-hosted/Managed | Self-hosted | | **Type safety** | N/A | Partial | Full (Zod) | | **SDK generation** | No | No | No | | **Auth handling** | OAuth 2.0 | Manual config | Env variables | | **Test clients** | Playground | No | HTML clients | ## The problem: When LLMs hallucinate with poor OpenAPI documentation Even the best AI models can confidently make things up when working with poorly documented APIs. This hallucination problem gets especially bad with MCP servers built from thin OpenAPI documents. Suppose we ask a seemingly straightforward question like, _"What was Lance Stroll's fastest lap at Silverstone?"_. Here's what can happen with insufficient API documentation, **especially problematic since Lance Stroll isn't even in the database**. ![Claude generating a fake lap time](/assets/mcp/optimizing/claude-hallucination.png) In this example, rather than returning an error or saying it doesn't know, Claude uses the `lapsPostLap` tool to create a completely new (and fictional) lap record for Lance Stroll at Silverstone. This happens because: 1. **Endpoint purpose is unclear:** Without explicit documentation about the purpose of each endpoint, the LLM can't determine which tool to use and when. 2. **Parameter documentation has gaps:** Vague parameter descriptions may lead the LLM to misjudge expected formats and values, resulting in incorrect assumptions about a tool's intended purpose. 3. **Context is missing:** Without examples of real response patterns, the AI can't infer what expected data looks like, resulting in incorrect assumptions about the API's behavior. 4. **Error guidance is insufficient:** Poor error documentation prevents the AI from recognizing when additional context or clarification is needed, leading to more hallucinations. The good news? These problems can mostly be avoided by structuring your MCP tools well and following a few simple guidelines when writing your OpenAPI document. ## How to optimize your OpenAPI document for MCP Regardless of which tool you choose, the quality of your resulting MCP tools depends on the quality of your OpenAPI document. AI agents need more context than human developers to use APIs effectively. Prompt engineering is most of the work when it comes to getting good results from AI models. Similarly, the clearer and more structured your OpenAPI documentation is, the more effectively AI tools will be able to understand and use your API. Try these simple strategies to enhance your OpenAPI document and make it easier for AI models to interact with your API through MCP. ### Write for AI agents, not just humans Humans can infer context from brief descriptions. AI agents cannot. Compare these descriptions: **Basic description** (for humans): ```yaml get: summary: Get task description: Retrieve a task by ID ``` **Optimized description** (for AI agents): ```yaml get: summary: Get complete task details description: | Retrieves full details for a specific task including title, description, priority level (low/medium/high), current status, assignee, due date, and creation/update timestamps. Use this endpoint when you need complete information about a task. If you only need a list of task IDs and titles, use listTasks instead. ``` The optimized version tells the AI agent what data to expect in the response, when to use this endpoint vs alternatives, and what each field means (priority values, status types). ### Provide clear parameter guidance Include examples and explain constraints: ```yaml parameters: - name: task_id in: path required: true schema: type: string format: uuid description: | The unique identifier for the task. Must be a valid UUID v4. You can get task IDs by calling listTasks first. examples: example1: summary: Valid task ID value: "550e8400-e29b-41d4-a716-446655440000" ``` ### Add response examples Example responses help AI agents understand what successful responses look like: ```yaml responses: "200": description: Task retrieved successfully content: application/json: schema: $ref: "#/components/schemas/Task" examples: complete_task: summary: Task with all fields populated value: id: "550e8400-e29b-41d4-a716-446655440000" title: "Review Q4 goals" description: "Review and update quarterly objectives" priority: "high" status: "in_progress" assignee: "alice@example.com" due_date: "2025-10-15" created_at: "2025-10-01T09:00:00Z" updated_at: "2025-10-07T14:30:00Z" ``` ### Use descriptive operation IDs Operation IDs become tool names. Make them clear and action-oriented: ```yaml # Good operationId: createTask operationId: listActiveTasks operationId: archiveCompletedTasks # Less clear operationId: post_tasks operationId: get_tasks_list operationId: update_task_status ``` Every API operation should have a unique, descriptive `operationId` that clearly indicates its purpose. This naming convention helps AI models accurately map natural language requests like, _"What's Hamilton's fastest lap?"_ to the correct tool, since the `operationId` is used as the default tool name. ### Document error responses Help AI agents understand what went wrong: ```yaml responses: "404": description: | Task not found. This usually means: - The task ID is incorrect - The task was deleted - You don't have permission to view this task content: application/json: schema: $ref: "#/components/schemas/Error" ``` ### Optimize MCP tools with x-speakeasy-mcp Speakeasy provides a dedicated OpenAPI extension specifically for customizing your MCP tools. The `x-speakeasy-mcp` extension gives you fine-grained control over how your API operations are presented to AI agents, allowing you to: - Override tool names and descriptions - Group related tools with scopes - Control which tools are available in different contexts Here's how to use this extension: ```yaml paths: /tasks: post: operationId: createTask summary: Create a new task x-speakeasy-mcp: name: "create_task_tool" description: | Creates a new task in the task management system. Use this when the user wants to add a new task or todo item. Requires a title and optionally accepts a description and priority level. scopes: [write, tasks] ``` This allows you to provide AI-specific descriptions and organize tools using scopes. #### Customize tool names and descriptions While a good `operationId` is great for SDK generation, MCP tools sometimes benefit from more descriptive names:
```yaml filename="openapi.yaml" "/race-results/summary": get: operationId: getRaceSummary summary: Get race summary description: Retrieve a summary of race results # Default MCP tool will use these values ```
```yaml filename="openapi.yaml" "/race-results/summary": get: operationId: getRaceSummary summary: Get race summary description: Retrieve a summary of race results x-speakeasy-mcp: name: "get_race_finish_positions" description: | Get the final positions of all drivers in a specific race. Returns each driver's finishing position, total time, and points earned. Use this tool when you need to know who won a race or how drivers performed. ```
The improved MCP tool name and description provide clearer guidance to AI agents about what the endpoint does and when to use it while preserving your API's original structure. #### Organize tools with scopes Scopes let you control which tools are available in different contexts: ```yaml paths: /tasks: get: x-speakeasy-mcp: scopes: [read] post: x-speakeasy-mcp: scopes: [write] /tasks/{id}: delete: x-speakeasy-mcp: scopes: [write, destructive] ``` Start the server with specific scopes: ```bash # Read-only mode npx mcp start --scope read # Read and write, but not destructive operations npx mcp start --scope read --scope write ``` If you're using Claude Desktop, you can also specify scopes in the config: ```json { "mcpServers": { "RacingLapCounter": { "command": "npx", "args": [ "-y", "--package", "racing-lap-counter", "--", "mcp", "start", "--scope", "read", "--scope", "write" ] } } } ``` #### Apply scopes across multiple operations To apply scopes consistently across many operations, you can use the global `x-speakeasy-mcp` extension: ```yaml x-speakeasy-mcp: scope-mapping: - pattern: "^get|^list" scopes: [read] - pattern: "^create|^update" scopes: [write] - pattern: "^delete" scopes: [write, destructive] ``` This automatically applies scopes based on operation name patterns, saving you from manually tagging each endpoint. ### Add detailed parameter descriptions and examples To make it easier for AI models to understand your MCP tools, provide detailed descriptions for each parameter. This includes specifying the expected format, constraints, and examples. This helps the LLM choose the right tools when it needs to follow a step-by-step approach to accomplish a task. ```yaml filename="openapi.yaml" parameters: - name: driver_id in: path required: true schema: type: string description: | The UUID of the driver to retrieve. Must be a valid UUID v4 format. format: uuid examples: hamiltonExample: summary: Lewis Hamilton's ID value: f1a52136-5717-4562-b3fc-2c963f66afa6 verstappenExample: summary: Max Verstappen's ID value: c4d85b23-9fe2-4219-8a30-72ef172e327b title: Driver Id description: The UUID of the driver to retrieve x-speakeasy-mcp: description: | The unique identifier for the driver. You can find the IDs of current F1 drivers by first using the listDrivers tool. Common drivers include Lewis Hamilton, Max Verstappen, and Charles Leclerc. ``` ### Add examples to responses To improve the LLM's understanding of your API's responses, provide detailed descriptions, examples, and expected structures. This helps the LLM accurately interpret the data returned by your API. ```yaml filename="openapi.yaml" responses: "200": description: | Lap records retrieved successfully, sorted from fastest to slowest lap time. Returns an empty array if the driver exists but has no recorded laps. content: application/json: schema: type: array items: "$ref": "#/components/schemas/Lap" title: DriverLapsResponse examples: multipleLaps: summary: Multiple lap records value: - id: "3fa85f64-5717-4562-b3fc-2c963f66afa7" lap_time: 85.4 track: "Silverstone" - id: "3fa85f64-5717-4562-b3fc-2c963f66afa8" lap_time: 86.2 track: "Monza" emptyLaps: summary: No lap records value: [] x-speakeasy-mcp: name: "get_driver_lap_records" description: | Returns an array of lap records for the requested driver. Each record contains: - id: A unique identifier for the lap record - lap_time: Time in seconds (lower is better) - track: Name of the circuit where the lap was recorded The records are sorted from fastest to slowest lap time. If the driver exists but hasn't completed any laps, this will return an empty array. ``` ## Conclusion Generating MCP servers from OpenAPI documents bridges the gap between your existing APIs and AI agents. With an OpenAPI document you can have a working MCP server in minutes instead of days. While this guide focuses on optimizing your OpenAPI document for MCP servers, these techniques are also good practice for writing well-structured, high-quality, comprehensive OpenAPI documents with great developer experience. Ready to get started? Try [Gram](https://app.getgram.ai) for instant managed hosting, explore [FastMCP](https://github.com/jlowin/fastmcp) for Python-based development, or use [openapi-mcp-generator](https://github.com/harsha-iiiv/openapi-mcp-generator) for a self-hosted TypeScript solution. For more information on how to improve your OpenAPI document, check out our [OpenAPI best practices guide](/docs/prep-openapi/best-practices). # MCP tools: Less is more Source: https://speakeasy.com/mcp/tool-design/less-is-more import Image from "next/image"; While building MCP servers, it's often tempting to include every single tool you think might be useful to someone someday. But [researchers at Southern Illinois University](https://arxiv.org/pdf/2411.15399) have found that this (lack of) strategy confuses LLMs and leads to worse performance. We recommend a more curated approach. By curating smaller MCP servers per use case, you can help your users install the MCP servers they need without overwhelming the LLM or depending on users to know how to curate their own MCP tools for each use case. ## What curated MCP servers look like The idea of curating MCP servers is to create smaller, focused servers that contain only the tools relevant to a specific use case or department. This reduces the cognitive load on users and helps LLMs perform better by limiting the number of tools they need to consider at any one time. Take a hypothetical example of a company that provides an MCP server for its internal customers. This server should feature tools used by the sales, marketing, and customer support teams. Tools could include: - `/getCustomer`: A tool for retrieving customer information - `/getProductInfo`: A tool for retrieving product details - `/getSalesData`: A tool for retrieving sales statistics - `/getMarketingCampaigns`: A tool for retrieving marketing campaign details - `/getSupportTickets`: A tool for retrieving customer support tickets - `/getFeedback`: A tool for retrieving customer feedback The naive approach would be to create a single MCP server that includes all these tools. What we're suggesting instead is creating separate MCP servers for each department: - **Sales MCP server**: Contains tools like `/getCustomer`, `/getProductInfo`, and `/getSalesData`. - **Marketing MCP server**: Contains tools like `/getMarketingCampaigns` and `/getProductInfo`. - **Customer support MCP server**: Contains tools like `/getSupportTickets` and `/getFeedback`. This way, each MCP server is tailored to the specific needs of its users, reducing confusion and improving the performance of the LLMs that interact with these tools.; Here's what that might look like in practice: ## Why curated MCP servers are better Recent research on **tool loadout**, the practice of selecting only relevant tool definitions for a given context, reveals concrete numbers about when LLMs start to struggle with having too many options. ### The research: When tool confusion kicks in In the article, [How to Fix Your Context](https://www.dbreunig.com/2025/06/26/how-to-fix-your-context.html#:~:text=When%20prompting%20DeepSeek,tool%20selection%20accuracy), Drew Breunig discusses two papers that found the following thresholds: For large models (like DeepSeek-v3): - **30 tools:** Thirty tools is the critical threshold at which tool descriptions begin to overlap and create confusion. - **100+ tools:** Models are virtually guaranteed to fail at tool selection tasks when choosing from over 100 tools. - **3x improvement:** Using RAG techniques to keep tool count under 30 resulted in dramatically better tool selection accuracy. For smaller models (like Llama 3.1 8B): - **19 tools:** A set of 19 tools is the sweet spot at which models succeed at benchmark tasks. - **46 tools:** A selection of 46 tools is the failure point at which the same models fail the same benchmarks. - **44% improvement:** Dynamic tool selection improved performance when the tool count was reduced. ### Real-world testing: The Dog API example To demonstrate this principle in action, we created a practical test using the [Dog CEO API](https://dog.ceo/) with [Gram](https://getgram.ai)-hosted MCP servers. The results clearly show how tool count affects LLM performance. We created an OpenAPI document for the Dog API, with an endpoint per dog breed. The full API had 107 dog breeds, so our OpenAPI document has 107 `GET` operations. We then uploaded the OpenAPI document to [Gram](https://getgram.ai) and created a single MCP server with all 107 tools, and installed this remote MCP server in Claude Desktop. On our very first test using Claude Sonnet 3.5, the LLM hallucinated an endpoint for **Golden Retrievers**, when in fact there is only a single **Retriever** endpoint. Although later tests using Claude Code and Claude Desktop with different models yielded better results, the initial confusion was evident. The effect of having 107 tools in one server caused Claude Desktop to frequently stop responding after one sentence with a generic error: > Claude's response was interrupted. This can be caused by network problems or exceeding the maximum conversation length. Please contact support if the issue persists. ![A screenshot of the Claude Desktop chat UI shows an error message and a brief conversation in which Claude agrees to get four images of dogs using a tool. The error message states: Claude's response was interrupted. This can be caused by network problems or exceeding the maximum conversation length. Please contact support if the issue persists.](/assets/mcp/less-is-more/claude-network-issue.png) Next, we decided to test the same MCP server using a smaller model on LM Studio. We used the `qwen/qwen3-1.7b` model, which is trained specifically for tool calling. With the same 107 tools, the model struggled to select a tool most of the time. It hallucinated incorrect tool names based on patterns it recognized in the tool names. ![A screenshot of the LM Studio UI shows a conversation where the qwen3-1.7b model fails to use correct tool names when presented with 107 dog breed API tools. The model attempts to call openapi_fetch_random_springer_photo and openapi_fetch_random_poodle_photo, but receives error messages stating it cannot find tools with those names. The interface shows multiple failed tool call attempts in red error text, demonstrating how having too many tools available causes smaller models to hallucinate incorrect function names.](/assets/mcp/less-is-more/lm-studio-107-tools-qwen.png) We then created several smaller MCP servers with fewer tools, each containing only a subset of the dog breeds. We tested these servers with the same `qwen/qwen3-1.7b` model in LM Studio. First, we created a server with 40 tools, which included a random selection of dog breeds. The model was able to successfully call three out of four tools, but still hallucinated one endpoint. ![A screenshot of the LM Studio UI shows a conversation where the qwen3-1.7b model successfully uses the correct tool names for three of four tool calls. It hallucinates one endpoint.](/assets/mcp/less-is-more/lm-studio-40-tools-qwen.png) Next, we created a server with 20 tools, which included a mixture of random dog breeds. The model got 19 out of 20 tool calls correct, with only one hallucinated tool call. ![A screenshot of the LM Studio UI shows a successful conversation where the qwen3-1.7b model correctly uses almost all tool names when presented with only 20 tools. The interface displays only one hallucinated tool call, demonstrating improved performance when the number of available tools is reduced to an optimal range for smaller language models.](/assets/mcp/less-is-more/lm-studio-20-tools-qwen.png) Finally, we created two servers with only 10 carefully selected tools each. One server included the most common dog breeds, and the other contained rare dog breeds. The model successfully retrieved images of four different dog breeds with correct tool names and no errors. ![A screenshot of the LM Studio UI shows a conversation where the qwen3-1.7b model successfully retrieves images of four different dog breeds with the correct tool names when presented with only 10 carefully selected API tools. The interface displays clean, successful tool calls without any error messages, demonstrating optimal performance when the number of available tools is reduced to a manageable set for smaller language models.](/assets/mcp/less-is-more/lm-studio-10-tools-qwen.png) We then created a new conversation with both the rare and common dog breed servers installed. These servers have ten tools each, but they are focused on different sets of dog breeds. The model was able to successfully use tools from both servers without any errors. ![A screenshot of the LM Studio UI shows a conversation where the qwen3-1.7b model successfully uses MCP tools from multiple curated servers. The interface shows clean tool calls to two specialized servers, one for rare breeds and one for common breeds. The conversation demonstrates how splitting tools across multiple focused MCP servers allows smaller models to maintain accuracy while accessing a broader range of functionality through targeted, domain-specific tool sets.](/assets/mcp/less-is-more/lm-studio-split-tools-qwen.png) ### Our test results We know this isn't an exhaustive test, nor is it the most rigorous scientific study. But this method demonstrates how you can quickly set up tests to compare the performance of LLMs with different tool counts and configurations. Here's a summary of our findings: **With 107 tools**, both large and small models struggled to select the correct tools, leading to frequent errors and hallucinations. **With 20 tools**, the smaller model got 19 out of 20 tool calls correct, with only one hallucinated tool call. **With 10 tools**, the smaller model successfully retrieved images of four different dog breeds with correct tool names and no errors. And most surprisingly, when **20 tools were split across two focused servers**, the model was able to successfully use tools from both servers without any errors. This shows that by curating MCP servers to contain only the most relevant tools for a specific use case, we can significantly improve the performance of LLMs, especially smaller models. ### Benefits beyond accuracy From our testing with Qwen 3.1 1.7B, we found that curating MCP servers improves accuracy and dramatically speeds up the response time and thinking time of the model. Parsing the prompt, selecting the right tools, and generating a response all happen much faster when the model has fewer tools to consider. This is especially important for real-time applications, where response time is critical. ## How to implement curated MCP servers We recommend following these steps to implement curated MCP servers: ### 1. Identify use cases Start by identifying the specific use cases or departments that will benefit from their own MCP servers. This could be based on job roles, projects, or specific tasks. ### 2. Select relevant tools per use case For each use case, select only the tools that are relevant to that specific context. Avoid including tools that are not directly applicable to the tasks at hand. ### 3. Create focused MCP servers Create separate MCP servers for each use case, ensuring that each server contains only the tools selected in the previous step. This will help reduce confusion and improve performance. ### 4. Test and iterate Use the approach demonstrated in our Dog API example: - Create test scenarios for your use cases. - Compare performance between monolithic and curated approaches. - Measure both accuracy and response time. - Gather user feedback on ease of use. ## Related reading For a real-world example of how tool proliferation affects MCP users, see our companion article: [Why less is more: The Playwright proliferation problem with MCP](/mcp/using-mcp/playwright-tool-proliferation). This article demonstrates how reducing the Playwright MCP server from 26 tools to just 8 essential ones dramatically improves agent performance and reduces decision paralysis in browser automation tasks. ## We're here to help At Speakeasy, we understand the importance of curating MCP servers for optimal performance. With [Gram's Toolsets feature](/docs/gram/concepts/toolsets), creating curated MCP servers is incredibly straightforward - just select the specific tools you need from any OpenAPI spec, and Gram automatically generates a focused MCP server. You can create multiple toolsets from a single API, each tailored to different use cases or departments, making it easy to implement the curated approach we've described here. # Filtering large MCP tool responses with jq Source: https://speakeasy.com/mcp/tool-design/response-filtering-jq Large API responses consume valuable LLM context. When an API returns hundreds of user records, the LLM must process every field of every record, even when only a few specific details are needed. This wastes context space and slows down response generation. ## The context window problem Consider a contact management API. A tool that calls `GET /contacts` might return 100 contacts, each with 20 fields (name, email, phone, address, company, etc.). That's 2,000 fields of data loaded into the LLM's context, when the user might only need email addresses. Traditional solutions involve manually filtering response data in the MCP server code. This creates a tradeoff: Either include all data (wasting context) or exclude fields (making data inaccessible when needed). ## What is jq? jq is a lightweight command-line JSON processor. Like `sed` for JSON data, jq slices, filters, and transforms structured data with minimal syntax. It's particularly effective for extracting specific values from complex API responses. ## Dynamic response filtering in MCP Response filtering allows the LLM to apply jq syntax to transform API responses based on the response schema. The LLM dynamically selects only the information needed to answer each query. ## Implementation with FastMCP Here's how to implement jq filtering in a FastMCP tool: ```python import subprocess import json from mcp.server.fastmcp import FastMCP mcp = FastMCP("Contact Manager") @mcp.tool() def get_contacts(jq_filter: str | None = None) -> str: """Retrieve contact information. Args: jq_filter: Optional jq syntax to filter the response """ # Fetch contacts from your API response = fetch_contacts_from_api() # Apply jq filter if provided if jq_filter: response = apply_jq_filter(response, jq_filter) return json.dumps(response) def apply_jq_filter(data: dict, filter_expr: str) -> dict: """Apply a jq filter to JSON data.""" try: # Convert data to JSON string json_input = json.dumps(data) # Run jq command result = subprocess.run( ['jq', filter_expr], input=json_input, capture_output=True, text=True, check=True ) return json.loads(result.stdout) except subprocess.CalledProcessError as e: return {"error": f"Invalid jq filter: {e.stderr}"} except json.JSONDecodeError: return {"error": "Failed to parse jq output"} ``` With this implementation, the LLM can apply jq filters dynamically: ```python # Extract only name and email fields get_contacts(jq_filter='.contacts[] | {name, email}') # Filter for active users only get_contacts(jq_filter='.data[] | select(.status == "active")') # Transform arrays of objects get_contacts(jq_filter='.results | map({id, company_name})') ``` ## How it works By adding an optional `jq_filter` parameter to your tool, the LLM can provide jq syntax to filter the response before processing it. For example, when asked "What are the email addresses of active customers?", the LLM can: 1. Call `get_contacts` with `jq_filter='.contacts[] | select(.status == "active") | .email'` 2. The tool applies the filter using the `jq` command-line tool 3. Only the filtered results are returned to the LLM 4. The LLM processes a fraction of the original response size This approach maintains full API access while dramatically reducing context consumption. The LLM chooses what data to retrieve based on each specific query. ## Related reading For more strategies to optimize MCP tools, see [Why less is more for MCP](/mcp/tool-design/less-is-more). # How AI agents can improve your existing systems Source: https://speakeasy.com/mcp/using-mcp/ai-agents/agent-use-cases Adding AI agents to real-world products today feels like adding "voice control" to your app in 2010: Everyone's doing it, every vendor promises the simplest solution, and the demos look magical. Companies rush to experiment with agents without a clear understanding of which problems these new tools solve better than existing solutions. The result? Widespread confusion. Vendor demos showcase agents performing impressive but isolated tasks, rarely demonstrating how they provide real-world value. Meanwhile, technical discussions focus on abstract capabilities rather than practical applications. The gap between potential and practice raises a key question: "Where exactly do agents fit into our business?" This guide aims to answer the question with concrete, real-world integration examples. Instead of theoretical agent architectures, we'll explore specific ways agents can enhance an existing system, from adding conversational interfaces to APIs and websites, to transforming how teams interact with data, manage inventory, and access organizational knowledge. We'll show you where agents create value, how they connect to your existing architecture, and what implementation approaches work in production environments. You'll see how agents **complement** rather than **replace** your current systems, allowing you to enhance capabilities incrementally without massive rebuilds. ## Using agents as a conversational interface to your APIs APIs are the backbone of your application, but connecting them to end users requires building a frontend. Traditional website interfaces like menus, forms, and multi-step processes create friction and limit what users can accomplish. Agents bridge this gap by creating a direct conversational layer between users and your backend APIs. Instead of learning your API structure or navigating complex interfaces, users simply express their needs in natural language, and the agent handles the technical translation and interaction. For example, say you have an API that allows users to search for products and place orders. Typically, a customer would navigate a website, find the product they want, and click a button to order it. To enable this, you'd need to create a frontend that lets users find products and place orders. With an agent, processes become conversations: ``` Customer: "I'm looking for a blue dress in size medium" Agent: → Calls product-search API with filters → "I found 6 blue dresses in size medium. The most popular is this A-line cotton dress at $79. Would you like to see all options?" ``` ``` Customer: "Where is my recent order?" Agent: → Requests order number and verifies it → Calls order tracking API → "Your order #45678 shipped yesterday via FedEx and should arrive by Thursday. Would you like the tracking number?" ```
![Flow diagram showing how an AI agent mediates between a user and existing API infrastructure](/assets/mcp/integrating-with-systems/website-integration.png)
The agent works as an interface layer, handling: - Intent recognition, in other words, understanding the user's request - Authentication and authorization - API calls - Information presentation By handling these layers of interaction, the agent shifts the user experience from transactional to exploratory, allowing users to ask follow-up questions and receive richer responses drawn from multiple sources. ## Using agents for data analysis Agents can bridge the gap between data stores and insights, enabling interactions that go beyond answering specific questions to discovering patterns. While many organizations collect vast amounts of data, turning that data into actionable insights is a common challenge. Traditional analysis requires specialists to create queries, build dashboards, and interpret results. Even self-service tools require users to understand data structures and visualization. Data analysis agents can translate natural-language questions into queries and visualizations. For example: ``` Product manager: "What's the overall sentiment about our new checkout process?" Agent: → Retrieves feedback data → Analyzes sentiment for checkout mentions → Categorizes issues → "Overall sentiment is 82% positive. Main complaints focus on mobile payment options (15%) and form validation errors (3%)." ``` ``` Product manager: "Why are people having issues with mobile payments?" Agent: → Filters for negative mobile payment feedback → Identifies patterns → "The most common issues are: payment processing times too long (60%), unsupported payment methods (25%), and UI rendering problems on specific devices (15%)." ``` The agent analyzes, interprets, and synthesizes insights from raw data.
![Diagram illustrating how a business user interacts with an analysis agent to retrieve insights](/assets/mcp/integrating-with-systems/data-analysis.png)
Implementing a data analysis agent requires: - Connecting the agent to data sources through APIs or database connectors. - Building a conversation memory system that maintains context between interactions. - Devising prompts that guide the agent in providing the right insights. - Adding a layer to format the output into a human-readable format or visualization. Data analysis agents are especially effective with unstructured data sources such as customer feedback, support tickets, and social media comments. They can identify patterns, sentiment trends, and emerging issues that traditional analytics might miss due to the qualitative nature of the content. Consider an agent that analyzes customer feedback and identifies patterns that can be used to improve your product. When multiple customers mention a particular issue, the agent not only identifies it but also provides context, such as how many customers are affected and how severe the problem is. ## Using agents for logistics and inventory Inventory management systems track stock and transactions but don't help users make decisions based on that data. Managers must balance storage costs, lead times, seasonal fluctuations, and supplier reliability. An agent can automate much of this. Here's an example: ``` Inventory manager: "What products should we restock this week?" Agent: → Analyzes inventory levels → Reviews sales data and patterns → Considers supplier lead times → "Based on current sales, I recommend restocking: - Product A (85 units) - Product B (120 units) - Product C (50 units) Products D and F show declining demand with sufficient stock." ``` The agent can also answer follow-up questions: ``` Inventory manager: "Why such a high number for Product B?" Agent: → "Product B sales are up 35% from last month, with historical data showing a 40% increase during upcoming seasonal events. The supplier lead time is 15 days, longer than our average." ``` The agent incorporates real-time inventory levels, supplier constraints, business rules, and seasonal factors into recommendations. It builds on the data analysis agent's capabilities by factoring in business rules and operational constraints. The agent can also suggest specific actions to the user. ## Using agents for knowledge management Internal operations and knowledge management are especially well-suited to agent integration. Organizational information is often scattered across wikis, documentation, Slack channels, code repositories, and project management tools. This leads to wasted time and frustration when employees can't find the information they need. A Slack-based agent can provide a unified interface to this internal knowledge. Instead of requiring teams to consolidate systems, the agent connects directly to these disparate tools within the communication platform employees already use daily. Here's an example of how the agent might be used: ``` Employee: "@companybot Where can I find our brand guidelines?" Agent: → Searches knowledge base → "The brand guidelines are in the Marketing folder on Drive. Here's the direct link: [link]. Last updated last month." ``` ``` Developer: "@companybot Summarize PR #1234" Agent: → Fetches PR data → "PR #1234 adds input validation to the user registration form. Changes span 3 files with 124 additions and 26 deletions. Tests included, CI passed. Awaiting review from @seniordev." ``` This architecture emphasizes breadth over depth. Workplace agents connect to multiple tools and repositories, retrieving and synthesizing information across systems. The implementation is similar to that of the data analysis agent, but uses a Slack bot in place of a chat interface and a knowledge base instead of a data source. New team members onboard faster, and existing employees save time previously lost to context switching. ## Building agent networks for complex tasks While single agents provide value, combining multiple agents in networks creates systems that model **organizational workflows**. Agent networks help when: - Tasks require expertise from **multiple domains**. - Workflows involve **sequential or parallel processes**. - Responsibility needs **clear boundaries** (for example, different scopes between sales, marketing, and support). A solo support agent would need to handle everything from basic questions to technical issues, often limited in its capacity to manage depth and breadth simultaneously. Agent networks divide responsibilities across specialists: ``` Customer: "My payment failed but I was charged anyway" Triage agent: → Identifies payment dispute requiring technical and billing knowledge → "I'll connect you with our technical support team to investigate this issue" → Routes to technical support agent with context Technical support agent: → Reviews payment logs → "I see the transaction was approved but our system shows an error. Let me check with our payments specialist." → Consults payments agent for specialized insight Payments agent: → Analyzes transaction details → "The payment gateway approved the transaction but our confirmation webhook failed. I'll issue a refund and fix the webhook." → Returns solution to technical agent Technical support agent: → "We identified the issue. The payment was approved but our system didn't record it correctly. We've issued a refund that will appear in 2-3 business days and fixed the underlying issue." ``` These agents collaborate to solve problems beyond their individual capabilities.
![Diagram illustrating collaborative agent-based customer support](/assets/mcp/integrating-with-systems/collaborative-agents.png)
Collaborative agent systems mirror how human teams work. The customer experience remains conversational while specialists work together behind the scenes. ## Integrating agents with automation tools Beyond agent networks, integrating with automation platforms extends agent capabilities by connecting them to hundreds of existing services. While traditional automation relies on rigid triggers and actions, agent-powered automation adds intelligence to workflow design and execution. Platforms like [Zapier](https://zapier.com) and [Make](https://make.com) provide API access to services from CRMs to project management tools, payment processors, and communication platforms. For example, imagine a lead qualification workflow. Without agents, you might configure rules like, "If lead fills form, add to CRM" or "If job title contains 'Manager,' assign high priority." When information doesn't fit predefined categories, this rule-based approach often fails. With agent-powered automation, the agent enhances automation by adding context, making inferences, and applying judgment to incomplete information: ``` Lead: [Submits form with incomplete information] Agent: → Analyzes submission → "This lead works at Acme Corp based on their email domain. They didn't specify company size or budget, but LinkedIn shows 200+ employees and they recently received Series B funding." → Categorizes as high-priority enterprise lead → Creates enriched CRM entry → Assigns to enterprise sales team → Triggers personalized outreach sequence ``` You can connect agents to automation platforms using their APIs. For example, [Zapier recently released an MCP server](https://zapier.com/mcp), simplifying the integration of agents with its extensive ecosystem of app connections. Agent-powered automation benefits marketing, sales, support, and operations teams by enabling workflows that adapt to real-world complexity without constant maintenance. Your existing automation platform continues to handle service integrations, while agents add **reasoning**. ## Model Context Protocol: A USB port for AI The patterns described here use different integration approaches, creating potential complexity as agent deployments grow. The [Model Context Protocol (MCP)](https://modelcontextprotocol.io/) addresses this by standardizing communication between agents and tools. Traditional API integrations require custom code for each connection – including authentication, request formatting, and response handling – leading to fragmentation and ongoing maintenance challenges. MCP standardizes these interactions through a common protocol that works across tools and services. Rather than building point-to-point integrations, systems connect to an MCP server that manages communication. Much like an SDK, the protocol standardizes: - Authentication and authorization - Tool discovery and capabilities - Request and response formats - Context management - Error handling MCP has seen a surge in adoption recently, with many tools and services [releasing their own MCP servers](https://mcpservers.org/). ## Getting started with agent integration This guide explores practical patterns for integrating agents with existing systems. Here are our top tips to get you started: - **Start with a single integration point where an agent can provide immediate value.** This might be a website chatbot or API wrapper that enhances user interactions with your system. - **Connect your agent to existing backend systems through APIs or database connectors.** This allows the agent to access and manipulate data without needing a complete overhaul of your infrastructure. - **Build a feedback loop to capture user interactions.** Store these conversations to improve your agent's performance over time and identify gaps in its capabilities. - **Define clear boundaries for the agent,** including what the agent should handle and when it should escalate to human operators. Document these boundaries for users and developers. - **Implement standardized communication protocols like MCP as your agent ecosystem grows** to ensure consistent interactions across your architecture. Implementing agents in this way doesn't require rebuilding your tech stack. Instead, it allows agents to enhance your existing systems with conversational interfaces that reduce friction and increase accessibility for all users. To learn more about AI agents, take a look at our other articles in this series: - [Introduction to AI agents](/mcp/ai-agents-intro) - [Agent architecture](/mcp/architecture-of-agentic-applications) # A practical guide to the architectures of agentic applications Source: https://speakeasy.com/mcp/using-mcp/ai-agents/architecture-patterns It's tempting to assume that the most important decision in building an AI product is choosing between GPT-4, Claude, or an open-source model, but this is a misconception. High-profile failures like [IBM Watson for Oncology](https://spectrum.ieee.org/how-ibm-watson-overpromised-and-underdelivered-on-ai-health-care#:~:text=The%20realization%20that,to%20work%20with) show that even the most advanced models can fall short when the architecture, integration, and overall system design aren't built for real-world complexity. A powerful AI model with poor architecture will burn through the budget, waste GPU time, create technical debt, and perform worse than a less capable model built on a strong architectural foundation. This article discusses the role of architecture in building effective agentic applications, describing the two core architectural models – single-agent and multi-agent – and how to think about workflow design, autonomy, and coordination. You'll also find common design patterns, real-world examples, and a decision framework to help you choose the right approach for your use case. ## First, understand what your system needs to do Before choosing an architecture, you should first define the end-to-end workflow your system needs to support. What must the system do from the first input to the final output, regardless of how it's implemented? Is the workflow short and linear, or long with multiple steps? Can some steps run in parallel, or are there strict dependencies between stages? Does the flow involve multiple roles, services, or types of data? Does the process require coordination, retries, or adaptability? Your system's workflow should shape its architecture and help you decide whether a single-agent setup is enough or a distributed, multi-agent design is needed to support its purpose. ## Single-agent architectures Single-agent architectures rely on one agent to handle the entire workflow from start to finish, making them well suited to simple, sequential workflows that require little coordination. The agent is responsible for reasoning (evaluating input and deciding what to do), planning (breaking goals into steps), executing (calling functions or APIs), and interacting with tools (using capabilities beyond the model, like databases or search). ![A high-level diagram of the single-agent flow](/assets/mcp/architecture-patterns/single-agent-high-level.png) With thoughtful design, single-agent architectures can manage more complex workflows under the right conditions. For example, transparent, open-source [SWE-agent](https://github.com/SWE-agent/SWE-agent) (inspired by [Devin](https://devin.ai/)) autonomously uses tools to fix issues in GitHub repositories, detect security vulnerabilities, and carry out other scripted workflows through a single control loop, without delegation or parallelism. Let's take a look at some common patterns in single-agent architecture and how even basic workflows can benefit from making the right structural choices. ### The single-agent pattern In its simplest form, a single-agent system reacts to a trigger, processes a task, and returns an output. No memory, planning, or external interaction is involved – only input, reasoning, and response. This pattern is useful for simple automation or prototypes and helps validate workflows and ideas when building agentic applications. ![Diagram of the single-agent pattern](/assets/mcp/architecture-patterns/single-agent-architecture.png) Open-source automation agent [`bumpgen`](https://github.com/xeol-io/bumpgen) is an example of a single-agent architecture that shows how even basic automation tasks benefit from thoughtful architecture. The agent watches your project for new package releases, fetches the latest versions, and creates automated pull requests to update them. The workflow is straightforward: detect, fetch, update. No coordination or parallelism is needed, so one agent can handle the full flow independently. ### The memory-augmented agent pattern This pattern is useful when your system needs to remember past context (like previous user interactions, historical data, or external states) to make better decisions. For example, imagine you're building an automatic reminder system that sends personalized nudges to users. A cron-based trigger runs daily, the agent queries past messages or actions from a vector memory store, and uses that context to generate tailored reminders. This setup allows the agent to respond with awareness of past events. ![Diagram of the memory-augmented agent pattern](/assets/mcp/architecture-patterns/single-agent-memory-augmented.png) ### The tool-using agent pattern Let's say you're building a customer support agent that handles invoice requests. When a user opens a support ticket, the agent fetches billing data, formats it, and returns a summary – steps it can't complete alone. It calls the billing API, processes the response, and completes the task automatically. You don't want to hardcode every API interaction to make this process work reliably inside the agent. Instead, you can introduce, for example, an [MCP (Model Context Protocol)](/post/build-a-mcp-server-tutorial) layer – MCP is a protocol that standardizes access to external tools and services. The MCP layer represents the tooling interface where all external interactions, mostly APIs, are abstracted and maintained. This way, the logic stays inside the agent, and the heavy lifting is delegated to tools. ![Diagram of the tool-using agent pattern](/assets/mcp/architecture-patterns/single-agent-tool-using-architecture.png) ### The planning-agent pattern A planning agent generates a multi-step plan based on the initial input, walks through each action sequentially, and adapts as necessary by understanding dependencies between tasks and tracking execution. One use case for the planning agent pattern is an AI onboarding assistant for a SaaS product: When a user signs up, the system must schedule a welcome email, set up a product tour, check in after three days, and escalate to human support if there's no engagement after a week. These steps don't just happen—they need to be planned and executed in the right order. ![Diagram of the planning-agent pattern](/assets/mcp/architecture-patterns/single-agent-planning-agent-architecture.png) The planning-agent pattern is useful for tasks that can't be completed in one step and require a sequence of coordinated actions. ### The reflection-agent pattern A reflection agent is useful for tasks that require improvement over time, in addition to execution. After completing an action, the reflection agent stores the results, compares them to goals or metrics, and updates its strategy. Over time, this feedback loop helps an agent become more effective, even without human intervention. Suppose you build a trading assistant that makes daily trades based on market signals. At the end of each day, the agent evaluates which trades performed well, which didn’t, and how to adjust the strategy going forward. ![Diagram of the reflection-agent pattern](/assets/mcp/architecture-patterns/single-agent-reflection-architecture.png) This pattern is useful when your system needs to learn from past outcomes to improve future performance. ## Multi-agent architectures Real-world workflows aren't simple. Once you've built an MVP or validated your flow with a single-agent setup, you'll likely need to scale, adding more reasoning, parallelism, precision, or specialization. That's when multi-agent architectures come in. In a multi-agent architecture, multiple agents collaborate to complete a complex workflow. Each agent owns a specific responsibility – planning, retrieval, analysis, or execution – and communicates with others to move the process forward. ![High-level diagram of the multi-agent flow](/assets/mcp/architecture-patterns/multi-agent-high-level.png) The open-source [TaskWeaver](https://github.com/microsoft/TaskWeaver) project from Microsoft follows the multi-agent pattern: Goals are broken down into subtasks – such as retrieving documents, summarizing them, and drafting outputs – and delegated across agents. A central orchestrator manages collaboration and how results are passed between agents. This setup is common: Most effective multi-agent systems include a coordinating agent that supervises task delegation. Whether it's called a lead agent, supervisor, or manager, this agent ensures the workflow stays organized. In a multi-agent setup, each agent is typically designed as a single-agent system with its own memory, tools, and decision logic. For example, in an AI trading system, you may have distinct agents responsible for: - Fetching live market data. - Performing technical and sentiment analysis. - Reflecting on past performance to adjust strategy. - Placing the trades via a broker API. In multi-agent architectures, the focus shifts from what each agent does to how they collaborate. ### The supervisor pattern The supervisor pattern is one of the more commonly used multi-agent architectures, and the core idea is straightforward: A single agent takes the lead. The supervisor agent receives a trigger, breaks the task into sub-tasks, and delegates each to a specialized agent, then ensures that agents run in the right order, with the right context, and that the output flows back properly. A practical application of this architecture might be a specialist appointment system for hospitals. When a patient initiates a request, the supervisor agent takes control of the entire workflow: - First, it checks availability through a scheduler agent. - Next, it retrieves the patient's medical records via a records agent. - Then, it sends the records to a summarizer agent to generate a concise overview. - Finally, it forwards the final summary to an email agent responsible for notifying the specialist. The supervisor pattern in a hospital appointment system might look like this: ![Diagram of the practical application of the supervisor pattern in a specialist appointment system for a hospital](/assets/mcp/architecture-patterns/multi-agent-supervisor-pattern.png) ### The hierarchical pattern An extension of the supervisor pattern, the hierarchical pattern is used when tasks are too complex or broad to be managed by a single supervisor. It introduces layers of coordination: A top-level agent handles the high-level goal and delegates parts of it to mid-level agents, which further break the work down and assign tasks to lower-level agents. This approach is useful in systems where responsibilities must be split across specialized teams or domains. For example, in an enterprise document processing system, a top-level agent may oversee the entire pipeline, delegating summarization to one mid-level agent and data extraction to another, each of which manages its own group of workers. ![Diagram of the hierarchical pattern](/assets/mcp/architecture-patterns/multi-agent-hierarchical-architecture.png) ### The competitive pattern The competitive pattern involves multiple agents independently working on the same problem, each proposing its own solution. A separate evaluator agent reviews all submissions and selects the most suitable one based on predefined criteria such as speed, accuracy, creativity, and cost-efficiency. This approach is useful when diversity of thought or redundancy can lead to better outcomes. It also adds robustness: If one agent fails or performs poorly, others may still succeed. A typical use case for the competitive pattern is generating marketing copy: Several agents generate different headlines or content snippets, and an evaluator chooses the one that best fits a set of brand guidelines through A/B test criteria. ![Diagram of the competitive pattern](/assets/mcp/architecture-patterns/multi-agent-competitive-architecture.png) ### The network pattern The network pattern has no lead agent. Each agent has its own tools and communicates directly with others to coordinate tasks. Frameworks like [OpenAI Agents](https://github.com/openai/openai-agents-python) and [Crew AI](https://www.crewai.com) are designed around this model. While the network pattern offers flexibility, it often proves impractical in real-world applications. Without a clear flow, agent-to-agent communication is unstructured, making the system hard to debug, unreliable, and costly to run. Each step may trigger an additional LLM call, increasing latency. For these reasons, the network pattern is generally not suited to production use, unless you specifically need a decentralized setup, such as in research or simulation environments. ![Diagram of the network pattern](/assets/mcp/architecture-patterns/multi-agent-network-architecture.png) ## Final thoughts Once you understand the architectural options for building agentic systems, the next step is choosing the one that fits your use case. We've covered a range of architectural patterns, from simple single-agent setups to more complex multi-agent designs. Our final recommendation is this: If your application follows a multi-agent architecture, consider including a **reflective agent**. While multi-agent systems benefit from improved precision, reasoning, parallelism, and task specialization, adding a reflective agent introduces a feedback loop that enables the system to learn and improve over time. This is one of the most effective ways to take advantage of the adaptive potential of AI. If you're building a simple MVP or prototype, start with a **tool-using agent**. It's practical, fast to implement, and already supports memory and tooling. Add an MCP layer for external API access, and you can move quickly. If your application calls for a multi-agent setup, the supervisor pattern is usually the best place to start. Other multi-agent patterns (like network, hierarchical, and competitive) can be useful, but only in specific contexts: - Choose the **competitive pattern** when you want multiple agents to propose different solutions and compare the outcomes. - Choose the **hierarchical pattern** when tasks are complex and need to be divided across worker groups under sub-supervisors. Here’s a quick reference to help you choose the right architecture for your use case. | **Criteria** | **Recommended architecture** | | ------------------------------------------------- | ------------------------------------------------------------------------------- | | MVP, prototype, or linear automation | **Single-agent:** Tool-using pattern | | Simple task needing context or history | **Single-agent:** Memory-augmented pattern | | Workflow requiring planning or sequencing | **Single-agent:** Planning-agent pattern | | System needing self-correction or improvement | **Single-agent:** Reflection-agent pattern or **Multi-agent:** Reflective agent | | Complex workflow requiring task delegation | **Multi-agent:** Supervisor pattern | | Workflow requiring many workers per task | **Multi-agent:** Hierarchical pattern | | Need for diverse solutions and output comparison | **Multi-agent:** Competitive pattern | | Decentralized system for research or benchmarking | **Multi-agent:** Network pattern | # Introduction to AI agents Source: https://speakeasy.com/mcp/using-mcp/ai-agents/introduction Unlike traditional software, AI agents can gather context, make decisions, and execute tasks without constant human supervision. These autonomous AIs can do work for you. AI agents that use large language models (LLMs) are the most useful and show the most promise, as they let you automate complex workflows using natural language. In this context, an AI agent is a system that uses an LLM to control the flow of processes. Many think of AI agents as fully autonomous systems capable of performing complex multi-step tasks with various tools, like a human. However, AI agents fall on an agentic spectrum: The more agentic the system, the more its LLM decides how it operates. You probably think that, in most contexts, more agentic systems are more useful. However, there are trade-offs with agentic systems, which can be slow, inaccurate, and unpredictable. It's best to start off simple and only add complexity as needed. You may find that you don't need an agentic system at all. This introduction to AI agents explains how they work, discusses how they're used, and clarifies common misconceptions. We'll also examine how AI agents can connect to external systems, such as databases and APIs, using structured protocols like [the Model Context Protocol (MCP)](https://modelcontextprotocol.io/). This article is the first in a series designed to take you from a basic understanding of AI agents to building your own agentic applications: - **What are AI agents?** - **Optimizing your OpenAPI document for MCP servers** - **Real-world agent use cases** - **The architecture of agentic applications** - **The agentic ecosystem: Frameworks and tools** - **Integration with agents** Let's start by learning the difference between agents, operators, and assistants. ## AI solutions Broadly speaking, we can group AI solutions into three distinct categories: assistants, agents, and operators. Although they all use AI under the hood, what sets them apart is the method and purpose of their interactions. ### Assistants Assistants are perhaps the most familiar category of AI solution. Conversational knowledge-based systems, such as ChatGPT or Siri, are built to interact with humans using natural language. They often use LLMs to answer queries, generate content, or even compose poetry and write essays. LLMs like ChatGPT are trained using massive datasets and machine learning to recognize patterns and relationships in data. When provided with input, LLMs generate text sequentially, producing responses one word or phrase at a time. At each step, the LLM evaluates potential words or phrases (by assigning each a likelihood of occurring next in the sequence) and selects the most probable option. This process typically produces a reasonable response. The assistant interaction flow can be simplified into four steps: ![Assistant interaction flow](/assets/mcp/ai-agents-introduction/assistant-interaction-flow.gif) LLM training data is static and can quickly become outdated. To address this limitation, you can use retrieval-augmented generation (RAG), a method that dynamically fetches relevant and up-to-date external data. This data is then injected into the prompt, providing the LLM with additional context so that it can produce more accurate responses. Adding RAG to an LLM doesn't make it an agent because it doesn't enable autonomy or give the LLM the ability to perform actions based on inputs. ### Agents Agents extend the capabilities of assistants by allowing them to conduct autonomous actions through digital interfaces, like APIs, in response to inputs. Need the current stock price to make a split-second trading decision? An agent can grab it for you. Have to schedule an appointment in your calendar, close a support ticket, or update records in a database? Agents can perform these tasks for you, too. Agents use tools to interact with external systems. AI platforms like OpenAI and Claude provide built-in tools, including web search and file retrieval. Some of these platforms also support function-calling tools that allow you to connect agents to your own code, much like you would call a tRPC procedure in a full-stack TypeScript application. Agents can also use knowledge and memory to retrieve and store external information, enabling them to carry out more accurate, up-to-date, and context-aware actions and responses. Knowledge refers to information stored externally (such as documentation or pre-existing data stored in a database), whereas memory refers to personalized, dynamic data that is built from interactions with users or contexts. ![AI Agent](/assets/mcp/ai-agents-introduction/ai-agent.png) Input can take the form of a simple task from a user or a complex goal that involves multiple steps. The interaction flow can also be triggered by an environment sensor, such as a thermometer. The agent interaction flow can be simplified into four steps: ![Agent interaction flow](/assets/mcp/ai-agents-introduction/agent-interaction-flow.gif) Once an agent has been provided with a task or a goal, it gets to work. Agents can plan and operate independently, although they may pause for human feedback at critical checkpoints or when blocked. The task may finish after completion or when a specific stopping condition, such as the maximum number of iterations, is reached. Agents may also be able to recover from errors. For example, Cursor's AI agent can read an error message and use it as a prompt to fix the error. ## Operators OpenAI recently released a research preview of [Operator](https://openai.com/index/introducing-operator/), an agent that can browse the web to perform tasks by using OpenAI's [Computer-Using Agent](https://openai.com/index/computer-using-agent/). We can classify the OpenAI Operator as a type of operator. Operators are a type of agent capable of interacting with graphical user interfaces. They can perform actions, such as clicking, scrolling, or typing, until a task is completed or user input is required to carry out sensitive actions like entering login details. Operators can perform common tasks such as navigating websites, filling out forms, and uploading documents. For example, they can be used to automate the process of ordering groceries for a recipe. They are particularly useful when APIs aren't accessible or when the fastest way to automate a task is to navigate through an existing user interface. The operator interaction flow can be simplified into four steps: ![Operator interaction flow](/assets/mcp/ai-agents-introduction/operator-interaction-flow.gif) OpenAI's [Computer-Using Agent](https://openai.com/index/computer-using-agent/) and Claude's [computer use](https://docs.claude.com/en/docs/agents-and-tools/tool-use/computer-use-tool) tools are examples of operators. They both perceive the screen by taking screenshots of the computer and adding them to the LLM's context. Here's a brief summary and list of example use cases for each AI system: ![AI systems summary](/assets/mcp/ai-agents-introduction/ai-systems-summary.png) ## The maturity of assistants, agents, and operators While, in theory, operators are more powerful than agents and agents are more powerful than assistants, this doesn't mean that operators are always the best choice. Assistants are well understood as models, and assistants like ChatGPT by OpenAI and Claude by Anthropic are considered mature systems. We understand the strengths and limitations of these AI assistants well, and they're already widely used in production settings. AI agents, on the other hand, are still maturing, and the ecosystem of agents is evolving rapidly. The risk of an agent doing the wrong thing is higher than that of an assistant. While an assistant could embarrass the company it represents or get it into legal trouble, an agent could take down an API by flooding it with requests or ruin a project by erroneously closing all its tickets. There's also a risk of biased outputs. Ethical considerations such as transparency, data privacy, and clear accountability must be actively addressed when developing and deploying these systems. Operators are the riskiest of all AI solutions. Because they may be given full access to a machine, and they can use a mouse and keyboard to do anything a human could do, the potential for damage is much higher. AI operators require safeguards to minimize the risk of their performing harmful actions. Claude's computer use tools require a sandboxed computing environment. OpenAI's Computer-Using Agent has protections against misuse, such as performing illegal actions and overlooking model mistakes. You can minimize model mistakes by adding user confirmations and limiting the tasks that operators can perform autonomously, such as preventing banking transactions. Their use is limited because most operators are still in preview or beta phases. ## Building agentic systems Anthropic released [a guide to building effective agents](https://www.anthropic.com/engineering/building-effective-agents), which shares insights gained from working with dozens of teams to build AI agents across different industries. Anthropic found that the most successful agents weren't using complex frameworks or specialized libraries but using simpler solutions with composable patterns. ### When to use agents It's best to start with a simple system and only add multi-step agentic systems when needed. You might find that you don't need an agentic system at all. You need to consider the trade-off between the efficient system performance and low cost of straightforward systems and the improved task performance of agentic systems. A more agentic system also means more risk. If an agent makes reasoning errors, hallucinates, or relies on unreliable external APIs or data, task performance may decrease. Agents work best in situations requiring adaptability and autonomy, but for tasks with clear, predefined steps, it's better to use a workflow. A workflow is a system that uses LLMs and tools to complete tasks via predefined code paths. ### Frameworks: Use with caution There are many AI agent frameworks, such as [LangChain](https://www.langchain.com/), which make agentic systems easier to build. Frameworks simplify calling LLMs, calling APIs, and generating responses, and some frameworks can create multi-agent workflows for complex use cases. There are even no-code tools that let you create agents through a UI without writing any code, such as Zapier's AI Agent Builder. However, these abstractions can make debugging more challenging if the underlying prompts and responses are obscured. Don't use a framework unless you need one. Start simple by using LLM APIs directly. If you use a framework, make sure you understand the underlying code, as it's a common source of errors. Read our blog post, [Building an AI agent with OpenAPI: LangChain vs Haystack](https://www.speakeasy.com/post/langchain-vs-haystack-api-tools#security), to learn how you can build an AI agent with two popular frameworks. In our experience, Haystack is the better choice for production-level systems and quick proof-of-concept builds. LangChain is more suited to projects that need greater flexibility and allow time for experimentation, as LangChain's official documentation is lacking. Even with these frameworks, we found the AI agents hallucinated endpoints and produced unexpected errors that would need to be handled in a production system. ### Common patterns for agentic systems There is a range of common patterns used in agentic systems, from augmented LLMs to fully autonomous agents that use multiple LLMs. These patterns include: - **Augmented LLMs**: LLMs enhanced with knowledge, tools, and memory form the basic building blocks of agentic systems. These augmentations can be implemented using MCP, which allows developers to create two-way connections between their AI assistants, data sources, and tools. - **Prompt chaining**: This technique breaks tasks down into sequential steps, in which each LLM call processes the output of the previous call. - **Routing**: In a routing process, inputs are classified and directed to specific tasks. An example use case is directing easy questions to smaller LLM models and harder questions to more capable models to optimize cost and speed. - **Parallelization**: Using this technique, independent subtasks are executed simultaneously to improve efficiency or to obtain diverse outputs. - **Orchestrator-workers**: In an orchestrator-worker workflow, a central LLM dynamically breaks down tasks, delegates them to worker LLMs, and compiles the results. - **Evaluator-optimizer**: In an evaluator-optimizer workflow, one LLM generates a response while another evaluates it and provides feedback. The process then loops, and the output is refined iteratively. These patterns can be combined or customized to fit different use cases. We recommend you start with the simplest patterns and add to them as needed. ## AI agent use cases Agents are best used for tasks that have clear success criteria, require conversation and action, and enable feedback loops. Customer support agents and coding agents show the most promise for widespread use. AI agents can enhance customer support chatbots with tools that can perform actions such as accessing customer data and issuing refunds. The success of an agent can be measured using the customers' responses. You can find AI agents integrated into popular coding environments and IDEs, such as VS Code, Cursor, JetBrains, Replit, and Windsurf. These agents improve developer productivity by generating code snippets, refactoring code, providing detailed code documentation, fixing bugs, and recommending best practices based on a project's context. You can even use them to create an MVP app using only prompts - a practice known as "vibe coding" - or set them up to interact with an external environment. For example, GitHub Copilot can create GitHub actions to automate dependency audits in a project. There are also companies pursuing ambitious general-purpose agents, such as [Devin](https://www.cognition.ai/introducing-devin), an agent that claims to be the first AI software engineer. Although Devin's approach is to do everything for you, this won't yet work without intervention due to the unreliable output of AI agents and LLM limitations. The [Microsoft Copilot Agents](https://support.microsoft.com/en-us/topic/introducing-copilot-agents-943e563d-602d-40fa-bdd1-dbc83f582466) provide an example of an enterprise use case for AI agents. These AI agents add to the capabilities of Microsoft 365 Copilot by connecting to your Microsoft 365 organization's knowledge and data sources. They assist users in performing and automating a variety of tasks. Here at Speakeasy, we create SDKs for your APIs using the OpenAPI Specification, which provides a structured format (JSON or YAML) for describing RESTful APIs. The quality of your SDK depends on the quality of your OpenAPI document. Our AI agent, Speakeasy Suggest, is a tool that automatically improves OpenAPI documents. It suggests and applies fixes, then outputs the modified document. Speakeasy Suggest is available through the [Speakeasy CLI](https://www.speakeasy.com/docs/speakeasy-cli/suggest/README) or as a [GitHub workflow](https://www.speakeasy.com/docs/workflow-reference). ## MCP [MCP](https://www.anthropic.com/news/model-context-protocol) is a new, reliable, and secure open-source standard that was developed by Anthropic for connecting AI assistants to external data sources and tools. LLMs alone can't interact directly with databases or APIs — they lack the necessary access to external resources. MCP fixes this by defining structured ways of giving LLMs access to these resources, including tools and resources. Tools are functions that an LLM can call to query databases or to connect to APIs. Resources provide supplementary data, such as documentation or information stored in a database. The MCP design includes servers and clients that communicate through structured protocols like JSON-RPC. An MCP server can run locally using standard input/output on your system or remotely (as a deployed service) using HTTP server-sent events for streaming communication. MCP clients, like the chat interface in Cursor Agent, initiate interactions by sending structured requests to MCP servers. A unique feature of MCP is its reflection capabilities, which allow clients to dynamically discover the tools and resources available from servers. This differentiates it from conventional API frameworks such as REST or tRPC. ![MCP](/assets/mcp/ai-agents-introduction/mcp.png) Applications that support MCP integrations include: - [**Claude**](https://claude.ai/download) - [**Cursor**](https://www.cursor.com/) - [**Cline**](https://cline.bot/) Each client provides varying levels of integration and capabilities when connecting with MCP servers. If you want to learn how to build an MCP server, take a look at our [guide to building an MCP server for Discord](https://www.speakeasy.com/post/build-a-mcp-server-tutorial). We demonstrate how to create an MCP server that connects with the Discord API, enabling it to read, send, and add reactions to specific messages. For educational purposes, we guide you through developing a server by hand. If you want to build a production-ready MCP server, you can use Speakeasy to [generate one automatically from your OpenAPI document](/docs/standalone-mcp/build-server). Built on OpenAPI, Speakeasy can also generate SDKs, Docs, Terraform, and MCP for your APIs. TypeScript SDKs generated with Speakeasy include an MCP server that gives AI agents access to your APIs. ## Conclusion AI agents let you build autonomous systems capable of performing complex tasks. However, increased capability comes with increased risk and complexity. Start simple and only introduce agentic capabilities to your projects as needed. Frameworks and protocols like MCP simplify connecting LLMs with external resources, but you should always consider trade-offs carefully. Understanding these tools and their limitations will help you build effective, reliable, and maintainable agentic systems. In the next article of this series, we'll explore how you can optimize your OpenAPI document for MCP servers to improve AI agent reliability and performance. # Installing MCP servers: A quickstart guide Source: https://speakeasy.com/mcp/using-mcp/installing-mcp-servers import { Callout, Screenshot } from "@/mdx/components"; The Model Context Protocol (MCP) connects your AI assistants to real-world tools and data sources. Instead of requiring you to manually copy and paste information between your tools and your AI assistant, MCP lets your AI assistant interact directly with databases, file systems, services like GitHub, and hundreds of other tools. This guide demonstrates how to install and configure the GitHub MCP Server in popular AI clients like Claude, Cursor, and Windsurf. For a deeper understanding of MCP fundamentals, check out our [introduction to MCP](/mcp/getting-started/intro) and [AI agents overview](/mcp/ai-agents/introduction). ## The GitHub MCP Server The [GitHub MCP Server](https://github.com/github/github-mcp-server) allows an AI assistant to interact with GitHub repositories, issues, pull requests, and more. Let's say you're working on a project and need to: - Check recent issues in a repository - Create a new pull request for a bug fix - Update the issue with your progress - Request a code review when done You'd need to switch between your AI chat, GitHub's web interface, your terminal, and back again. With MCP, you can do all of this through a single conversation with your AI assistant, and if your client is agentic, it can even use MCP tools step by step to complete tasks on your behalf. With GitHub's MCP server installed, you could ask: > Show me the highest priority open issues in my repo, create a pull request for issue #42, and let me know when you're ready for me to start coding. Your client would then: - Connect to GitHub through the MCP server - Fetch and display the current issues - Create a draft pull request using GitHub's API - Keep track of the context for the entire workflow ## How to install MCP servers In most clients, installing MCP servers requires manual configuration through JSON config files. While this involves editing configuration files, the process is well-documented and gives you precise control over which tools your AI assistant can access. For GitHub, we'll use the local Docker installation, which gives us full control over the server instance and doesn't require external dependencies. This approach works consistently across all clients and keeps our data local. Below, we provide step-by-step instructions for the most popular clients using the Docker-based GitHub MCP Server. ### System requirements Before you begin, ensure you have the following prerequisites installed: - **Docker**: Download Docker Desktop from [docker.com](https://www.docker.com/). - **A GitHub personal access token**: Create a [personal access token](https://github.com/settings/personal-access-tokens/new) with the scopes you need for your workflow (such as `repo`, `read:org`, and `read:user`). Run the `docker --version` command in your terminal to verify that Docker is installed and running on your machine. Alternatively, you can use the instructions for the [remote-hosted version of the GitHub MCP Server](https://github.com/github/github-mcp-server/blob/main/docs/host-integration.md), which doesn't require the use of Docker. However, for this guide, we'll focus on the local Docker setup. ## Claude Adding MCP servers to Claude Desktop requires manual configuration through a JSON config file. While this requires more setting up than a one-click installation, it gives you full control over which servers and tools you enable. ### Install the GitHub MCP Server in Claude Let's take a look at how to install the GitHub MCP Server in Claude. #### Open the Claude settings Start by accessing the Claude settings through the main menu. On macOS, click **Claude -> Settings...** from the menu bar. On Windows, access the settings through the application menu. #### Access the developer configuration in Claude Navigate to the **Developer** section in the left sidebar of the **Settings** window. Click **Edit Config** to open the MCP configuration file. This will create or open the configuration file at the following location: - **In macOS**: `~/Library/Application Support/Claude/claude_desktop_config.json` - **In Windows**: `%APPDATA%\Claude\claude_desktop_config.json` #### Add the GitHub MCP Server configuration for Claude Replace the file content with the following configuration, substituting your actual GitHub personal access token: ```json { "mcpServers": { "github": { "command": "docker", "args": [ "run", "-i", "--rm", "-e", "GITHUB_PERSONAL_ACCESS_TOKEN", "ghcr.io/github/github-mcp-server" ], "env": { "GITHUB_PERSONAL_ACCESS_TOKEN": "" } } } } ``` #### Restart Claude and test the server configuration Save the configuration file, close the Claude app completely, and then reopen it. Once restarted, click **🔨** (the hammer icon) to see the available tools and test the connection by asking: > What repositories do I have access to? Claude will ask for your permission before executing any GitHub Actions. You'll see a confirmation dialog for operations like creating issues or branches. After configuring the GitHub MCP Server, you can use natural language commands to interact with your repositories directly from Claude. If the hammer icon fails to appear, it may be due to a syntax error in your JSON configuration. Double-check the syntax and ensure the file is saved correctly. Make sure to restart Claude Desktop completely after making changes to the configuration file. ## Cursor IDE Cursor integrates MCP servers through a simple configuration file in the project directory, giving you precise control over which tools are available for each project. As of Cursor version 1.0, MCP support is built-in, allowing you to easily add and manage servers from Cursor's settings. ### Install the GitHub MCP Server in Cursor Although Cursor has a built-in MCP server configuration for GitHub, for consistency across clients, we'll show you how to set it up manually using the MCP configuration. #### Access the MCP configuration in Cursor Open Cursor and navigate to **Settings -> Cursor Settings**, then open the **Tools & Integrations** tab. Look for the MCP section and click the **Add Custom MCP** button to access the configuration interface. #### Add the GitHub MCP Server configuration for Cursor This opens Cursor's global MCP configuration file. Add the GitHub MCP Server configuration along with your personal access token: ```json { "mcpServers": { "github": { "command": "docker", "args": [ "run", "-i", "--rm", "-e", "GITHUB_PERSONAL_ACCESS_TOKEN", "ghcr.io/github/github-mcp-server" ], "env": { "GITHUB_PERSONAL_ACCESS_TOKEN": "YOUR_GITHUB_TOKEN_HERE" } } } } ``` #### Use GitHub tools in Cursor Once configured, access the GitHub MCP functionality through Cursor's AI chat by clicking the chat icon or using `cmd + l` or `ctrl + l`. Cursor automatically discovers available MCP tools and allows you to interact with them using natural language commands. You can now make requests like, _"Show me recent issues in this repository,"_ or check on your workflow with queries like _"Which PRs are waiting for my review?"_ ## Windsurf IDE Installing an MCP server in Windsurf is fairly similar to installing one in Cursor. ### Access the Windsurf settings Click **Windsurf** in the top menu bar, then navigate to **Settings -> Windsurf Settings** to open the main configuration interface. ### Open the Windsurf plugin management Under the **Plugins (MCP Servers)** section, click **Manage plugins** to access the plugin configuration area. In the **Manage plugins** page, click **View raw config** to open the configuration file editor. ### Configure the GitHub MCP Server for Windsurf A new file, named `mcp_config.json`, will open in the editor. Replace the contents with this GitHub MCP Server configuration: ```json { "mcpServers": { "github": { "command": "docker", "args": [ "run", "-i", "--rm", "-e", "GITHUB_PERSONAL_ACCESS_TOKEN", "ghcr.io/github/github-mcp-server" ], "env": { "GITHUB_PERSONAL_ACCESS_TOKEN": "YOUR_GITHUB_TOKEN_HERE" } } } } ``` Save the `mcp_config.json` file after adding your configuration. ### Activate the GitHub plugin in Windsurf Return to the **Manage plugins** tab and click the **Refresh** button to reload the plugin configuration. The GitHub plugin will now appear in your available plugins. It is ready to use. You can now interact with your repositories, issues, and pull requests directly from Windsurf using natural language commands. ## VS Code with GitHub Copilot VS Code supports MCP through GitHub Copilot with version 1.101 or later. ### Install the GitHub MCP Server in VS Code Installing the GitHub MCP Server in VS Code requires configuring the MCP settings file to run the server as a Docker container with GitHub authentication. #### Access the MCP configuration via VS Code Open your VS Code settings and search for `MCP`. Ensure you have **Chat -> MCP** enabled. Under the **MCP** setting, click the **Edit in settings.json** link to open the configuration file. #### Configure the GitHub MCP Server for Copilot Add the following configuration to your VS Code MCP `settings.json` file: ```json { "servers": { "github": { "command": "docker", "args": [ "run", "-i", "--rm", "-e", "GITHUB_PERSONAL_ACCESS_TOKEN", "ghcr.io/github/github-mcp-server" ], "env": { "GITHUB_PERSONAL_ACCESS_TOKEN": "${input:github_token}" } } }, "inputs": [ { "type": "promptString", "id": "github_token", "description": "GitHub Personal Access Token", "password": true } ] } ``` The `${input:github_token}` placeholder will prompt you to enter your GitHub personal access token when the server starts. Save the configuration file when done. #### Start the GitHub MCP Server Open the Command Palette (`ctrl + shift + p` or `cmd + shift + p`) and type `MCP: List Servers` to see available servers. From the list, click on the GitHub MCP Server to start it. VS Code will prompt you to enter your GitHub personal access token. Enter your token and press `enter`. The server will start and display output indicating a successful startup. #### Use GitHub tools with Copilot Once configured, the GitHub MCP tools become available in the Copilot chat interface. You can enable and disable specific tools as needed by clicking the tool icon in the chat window. You can now interact with your repositories, issues, and pull requests directly through Copilot using natural language commands. ## Beyond GitHub: Popular MCP servers Once you're comfortable with GitHub integration, explore these popular Docker-based MCP servers and the development tools they provide: - **[The Docker MCP server](https://github.com/ckreiling/mcp-server-docker)**: Manage Docker containers with agents. ```json { "mcpServers": { "docker": { "command": "uvx", "args": ["mcp-server-docker"] } } } ``` - **[The PostgreSQL MCP server](https://github.com/modelcontextprotocol/servers-archived/tree/main/src/postgres)**: Manage databases and execute read-only queries. ```json { "mcpServers": { "postgres": { "command": "docker", "args": ["run", "-i", "--rm", "mcp-server-postgres"], "env": { "DATABASE_URL": "postgresql://localhost/mydb" } } } } ``` ## Troubleshooting common issues Here are some common problems and solutions to help you resolve any issues you encounter while installing or using MCP servers: ### Authentication problems #### GitHub OAuth failures If you see errors like `Invalid OAuth token` or `Insufficient permissions`, you can fix these errors by using a personal access token instead of OAuth: ```bash "env": { "GITHUB_PERSONAL_ACCESS_TOKEN": "ghp_your_token_here" } ``` #### Insufficient token permissions If you receive a `Token permissions insufficient` error: - Ensure your token has the required scopes (usually `repo` and `read:org`) - Check whether your organization requires SSO ### Connection problems #### The server won't start If your MCP server doesn't start - Check that Docker is installed and running - Verify that the server package is properly installed - Look at the error logs in your client #### The commands don't work If your commands aren't working: - Verify the server is running by looking for a green indicator in the client - Check the available tools to confirm the server has been installed correctly (each server exposes different capabilities) - Try simpler commands like, _"List repositories,"_ before trying more complex prompts # Use Hubspot from your MCP Client Source: https://speakeasy.com/mcp/using-mcp/mcp-server-providers/hubspot This guide shows you how to connect your HubSpot CRM data to Claude Desktop using the Model Context Protocol (MCP). Once connected, you can analyze deals, update contacts, and manage your sales pipeline using natural language commands. With the HubSpot MCP Server set up in Claude Desktop, you can ask Claude to "show me all deals in the negotiation stage" or "update the contact information for John Smith" and get immediate results instead of navigating through the HubSpot interface. ## Prerequisites - A [HubSpot account](https://www.hubspot.com) with admin access - An MCP Client like [Claude Desktop](https://claude.ai/download) ## Setting up the HubSpot MCP Server The HubSpot MCP Server enables real-time connection between Claude and your HubSpot CRM data. To set up this connection, you'll need to create a "private application" in HubSpot and configure it with the necessary permissions. Once created, HubSpot will provide an API key that the MCP server will use to access your CRM data. ### Creating a HubSpot private application On the HubSpot dashboard, click the settings icon in the top navigation bar. ![HubSpot settings icon in the top navigation bar](/assets/mcp/using-mcp/hubspot-claude-quickstart/hubspot-settings-icon.png) In your HubSpot account settings, go to **Integrations** > **Private Apps** and click **Create a private app**. ![HubSpot private apps page with "Create a private app" button](/assets/mcp/using-mcp/hubspot-claude-quickstart/hubspot-create-private-app.png) Enter a name for the application. ![HubSpot private app creation form showing app name field](/assets/mcp/using-mcp/hubspot-claude-quickstart/hubspot-create-private-app-name.png) Navigate to the **Scopes** tab and add the following scopes: - `crm.lists.read` and `crm.lists.write` - `crm.objects.companies.read` - `crm.objects.contacts.read` and `crm.objects.contacts.write` - `crm.objects.deals.read` and `crm.objects.deals.write` - `crm.objects.appointments.read` and `crm.objects.appointments.write` - `crm.objects.leads.read` and `crm.objects.leads.write` - `crm.objects.custom.read` and `crm.objects.custom.write` ![HubSpot private app scopes configuration showing CRM permissions](/assets/mcp/using-mcp/hubspot-claude-quickstart/hubspot-create-private-app-scopes.png) Add additional scopes based on the specific HubSpot features Claude needs to access in your workflow. Click **Create app** in the top-right corner and validate the creation. In the modal that opens, copy the API key and store it safely. You'll use this key when you add the MCP server to Claude Desktop. ![HubSpot API key modal displaying the generated private app access token](/assets/mcp/using-mcp/hubspot-claude-quickstart/hubspot-api-key-modal.png) ### Adding the HubSpot MCP Server to Claude Desktop Now update your Claude Desktop configuration to include the MCP server. In **Settings**, go to **Developer** > **Edit Config**. ![Claude Desktop configuration settings](/assets/mcp/using-mcp/hubspot-claude-quickstart/claude-desktop-config-settings.png) In the `claude_desktop_config.json` file that opens, add the HubSpot MCP Server configuration: ```json { "mcpServers": { "HubspotMCP": { "command": "npx", "args": ["-y", "@hubspot/mcp-server"], "env": { "PRIVATE_APP_ACCESS_TOKEN": "YOUR_HUBSPOT_KEY" } } } } ``` Replace `YOUR_HUBSPOT_KEY` with the API key you copied from HubSpot. Restart Claude Desktop to load the server. ## Testing the connection In Claude Desktop, start a new chat. Click the **Search and tools** button to see the HubSpot MCP Server listed. Enable all tools if they're disabled. ![Claude Desktop settings showing HubSpot MCP tools enabled](/assets/mcp/using-mcp/hubspot-claude-quickstart/claude-enable-hubspot-tools.png) Ask Claude to list the current contacts in your HubSpot application. ![Claude Desktop conversation showing successful HubSpot contact retrieval](/assets/mcp/using-mcp/hubspot-claude-quickstart/claude-hubspot-integration-test.png) ## Conclusion Claude can now access and update your HubSpot data through natural language conversations. Here are some ways to get more from your setup. ### Expand integration capabilities - Add more scopes to your private app for custom objects, advanced reporting, or marketing tools. - Explore other MCP servers from the [official MCP servers repository](https://github.com/modelcontextprotocol/servers) to connect more tools in your sales stack. ### Optimize your workflow - Create [custom slash commands](https://docs.claude.com/en/docs/claude-code/sdk/sdk-slash-commands#creating-custom-slash-commands) for common queries like pipeline analysis or contact updates. - Set up regular data reviews using Claude's analytical capabilities. - Train your team on natural language commands for faster CRM interactions. ### Learn more about MCP - Explore the [Model Context Protocol documentation](https://docs.anthropic.com/en/docs/mcp) to understand advanced connection patterns. - Check the [HubSpot MCP Server documentation](https://www.npmjs.com/package/@hubspot/mcp-server) for additional configuration options and troubleshooting. # Use Intercom from your MCP Client Source: https://speakeasy.com/mcp/using-mcp/mcp-server-providers/intercom [Intercom](https://www.intercom.com) is a customer messaging platform that helps teams manage support conversations and customer relationships. Using the Intercom MCP Server, you can analyze customer conversations through Claude Desktop, identifying churn risks and sentiment patterns by describing your needs in natural language. ![Intercom inbox showing multiple conversations with varying customer health signals](/assets/mcp/using-mcp/intercom/intercom-inbox-demo.png) At the end of this guide, you'll know how to connect the Intercom MCP Server to Claude Desktop. ## Prerequisites Before we begin, make sure you have: - [Claude Desktop](https://claude.ai/download) installed - An [Intercom workspace](https://www.intercom.com/) with admin access or permission to create integrations - A few sample conversations in Intercom (we'll show you how to set this up) ## Set up demo data in Intercom For this guide, we need realistic customer data to analyze. We'll use the Intercom Messenger chat widget to create sample conversations. ### Install the Intercom Messenger component We can't create conversations directly in Intercom, so we need to use the Messenger widget component. ![Intercom Messenger installation page showing code snippet options](/assets/mcp/using-mcp/intercom/intercom-messenger-install.png) Navigate to **Settings** > **Channels** > **Messenger** in your Intercom workspace. Under **Install** you'll see installation options for different platforms including React, Angular, Vue, WordPress, and others. Choose the option that matches your application and follow the instructions to install the widget. ### Create sample customer conversations Using the Messenger widget, create several conversations that represent different customer health scenarios, ranging from satisfied customers asking feature questions to frustrated users reporting technical issues and requesting cancellations. Here's an example of what one of these messages might look like: ![Example conversation showing a customer requesting subscription cancellation](/assets/mcp/using-mcp/intercom/intercom-inbox-demo.png) James Wilson is a customer who is actively seeking to cancel his subscription due to poor ROI. This is a critical churn signal that needs immediate attention. Create a few more messages with varying sentiment levels to build a realistic representation of what a messy inbox. Once you've created these messages, your Intercom inbox should look like this: ![Intercom inbox with multiple customer conversations showing various sentiment levels](/assets/mcp/using-mcp/intercom/intercom-final-inbox.png) ## Install and configure the Intercom MCP server We'll use the Intercom MCP server to connect your Intercom data to Claude Desktop. ### Get your Intercom access token First, create an Intercom app to access your data: - In Intercom, go to **Settings** > **Integrations** > **Developer Hub**. ![Navigating to developer hub](/assets/mcp/using-mcp/intercom/intercom-developer-hub-navigation.png) - Click the **New app** button and create an internal integration. - Name it `Customer Health Monitor` and click **Create App**. - Under **Permissions**, enable the following: - **Read conversations:** To view conversations - **Read one user and one company:** To list and view a single user and company - **Read events:** To list all events belonging to a single user - **Write events:** To submit events (user activity) - If that's not the case, click the **Edit** button to enable the permissions above. - Copy your **Access Token** from the authentication section. ![Copying the Access token](/assets/mcp/using-mcp/intercom/intercom-access-token-copy.png) ### Configure Intercom MCP server in Claude Desktop Update your Claude Desktop configuration to include the MCP server. In **Settings**, go to **Developer** > **Edit Config**. ![Claude Desktop configuration](/assets/mcp/using-mcp/intercom/claude-desktop-config-settings.png) In the `claude_desktop_config.json` file that opens, add the Intercom MCP Server configuration: ```json { "mcpServers": { "intercom": { "command": "npx", "args": [ "mcp-remote", "https://mcp.intercom.com/mcp", "--header", "Authorization:${AUTH_HEADER}" ], "env": { "AUTH_HEADER": "Bearer YOUR_INTERCOM_ACCESS_TOKEN" } } } } ``` Replace `YOUR_INTERCOM_ACCESS_TOKEN` with the access token you copied from the Intercom authentication page in the previous step. Restart Claude Desktop. When you first try to use the Intercom tools, Claude requests permission to access them. Click **Allow always** or **Allow once** to proceed. ## Testing the connection With the Intercom MCP server configured, you can now ask Claude to analyze your customer conversations. Test the integration with the following prompt: ``` List my recent Intercom conversations and identify any customer health signals. Summarize and give me a list of customers that are at risk of churning. ``` Claude will use the Intercom MCP server to get the list of conversations, identify the conversations that are at risk of churning, and summarize the conversations. ## Automate your workflow Once you've gotten the hang of it, you can automate your workflow by: - **Creating a weekly reminder** to run the health check. - **Setting up templates** for common outreach scenarios. - **Building a simple dashboard** to track health score trends over time. Ask Claude: ``` Create a follow-up workflow for next week. Set a reminder to run this analysis again and track how our outreach has affected the health scores. ``` ## Conclusion Now that you can analyze customer conversations in Claude Desktop using the Intercom MCP Server, try combining this functionality with other MCP servers. For example, you could use the [Slack integration](/mcp/using-mcp/mcp-server-providers/slack) to automatically notify your customer success team about urgent cases. # pandadoc Source: https://speakeasy.com/mcp/using-mcp/mcp-server-providers/pandadoc PandaDoc is a document automation platform that helps users create professional documents from templates. Using the PandaDoc MCP Server, you can create and populate professional documents through Claude Desktop, generating contracts, proposals, and reports by describing what you need in natural language. ![Screenshot of Claude Desktop showing the generated content for the document](/assets/mcp/using-mcp/pandadoc-claude-quickstart/final-result.png) This guide shows you how to connect the PandaDoc MCP Server to Claude Desktop. ## Prerequisites - A [PandaDoc account](https://www.pandadoc.com/) - [Claude Desktop](https://claude.ai/download) ## Creating a PandaDoc template PandaDoc creates documents from templates, so you'll need a template that Claude can populate with your content. In the PandaDoc dashboard, click **Templates** in the sidebar, then **+ Template** (or **Other Template Type**, if you're using a new account). ![Screenshot of the PandaDoc dashboard highlighting the new template button](/assets/mcp/using-mcp/pandadoc-claude-quickstart/add-template.png) Select **Blank Template**. ![Screenshot of the PandaDoc dashboard highlighting the blank template option](/assets/mcp/using-mcp/pandadoc-claude-quickstart/blank-template.png) Double-click the template name in the top-left corner and rename the template "Progress Report Template". This will make it easy to reference the template when you ask Claude to create the document using PandaDoc. ![Screenshot of the PandaDoc dashboard highlighting the template name and "Create document" button](/assets/mcp/using-mcp/pandadoc-claude-quickstart/rename-template.png) Click **Create document**. ## Retrieving the PandaDoc API key In the PandaDoc dashboard, navigate to **Settings → API and Integrations**. Scroll down and click **API** under **API and Webhooks**. ![Screenshot of the PandaDoc "API and Integrations" configuration highlighting the "API" option](/assets/mcp/using-mcp/pandadoc-claude-quickstart/api-settings.png) On the API configuration page that opens, you can view the Sandbox and Production keys. Click **Generate** to create a Sandbox key, then copy the key and save it. You'll need it for the Claude Desktop configuration. ![Screenshot of PandaDoc settings showing where to copy the generated Sandbox key](/assets/mcp/using-mcp/pandadoc-claude-quickstart/sandbox-api-key.png) ## Connecting the PandaDoc MCP Server Now add the PandaDoc MCP Server to the Claude Desktop configuration. In Claude Desktop, go to **Settings** → **Developer** → **Edit Config**. ![Screenshot of Claude Desktop highlighting the navigation path: Settings → Developer → Edit Config](/assets/mcp/using-mcp/pandadoc-claude-quickstart/claude-desktop-config-settings.png) Add the PandaDoc configuration to the `claude_desktop_config.json` file that opens: ```json { "mcpServers": { "pandadoc": { "command": "npx", "args": [ "mcp-remote", "https://developers.pandadoc.com/mcp", "--header", "Authorization: API-Key ${AUTH_TOKEN}" ], "env": { "AUTH_TOKEN": "YOUR_SANDBOX_KEY" } } } } ``` Replace `YOUR_SANDBOX_KEY` with the sandbox key you copied from PandaDoc. Restart Claude Desktop. ## Configuring Claude for optimal PandaDoc integration Update your response preferences in Claude Desktop to ensure Claude works effectively with PandaDoc templates. Go to **Settings** → **Profile** and add the following instructions to your response preferences: ```txt When working with the PandaDoc MCP Server: - Always create text fields with specific merge_field names when populating templates - Use the pattern: "Add a text field with merge_field name '[field_name]'" - Include verification: "Check that the document is filled, otherwise make sure" - Specify content length for better generation (e.g., "50-word report") ``` ![Screenshot of Claude Desktop settings showing the response preferences field](/assets/mcp/using-mcp/pandadoc-claude-quickstart/claude-preferences.png) ## Testing the connection Test the connection by asking Claude to find your PandaDoc template: ```txt Confirm the Progress Report template exists using the PandaDoc MCP server. ``` ![Screenshot of Claude Desktop UI showing a response confirming the PandaDoc template has been found](/assets/mcp/using-mcp/pandadoc-claude-quickstart/find-template.png) ## Generating and sharing a document Now ask Claude to generate a short scrum report, and create and share the document using PandaDoc: ```txt Before starting this task, first review my PandaDoc preferences and confirm you understand them. Then create a short scrum report and create a document in PandaDoc with the Progress Report template following my documented approach. Finally, publish and share the document with an_email_address_com. ``` ![Screenshot of Claude Desktop UI showing the report has been successfully generated and emailed](/assets/mcp/using-mcp/pandadoc-claude-quickstart/generate-document.png) Claude will generate sample report content, populate your template, create the document in PandaDoc, and share it with the specified email address. ![Screenshot of the email sent from PandaDoc with a link to the generated document](/assets/mcp/using-mcp/pandadoc-claude-quickstart/result-email.png) In a real-world scenario, you could provide the report content as part of the prompt, include a context document containing the data, or have Claude pull the information from another MCP server. ![Screenshot of the generated document in PandaDoc](/assets/mcp/using-mcp/pandadoc-claude-quickstart/result-doc.png) ## Conclusion Now that you can generate documents in Claude Desktop using the PandaDoc MCP Server, try combining this functionality with other MCP servers. For example, you could use [Slack conversations](/mcp/using-mcp/mcp-server-providers/slack) to automatically generate project reports or client updates with PandaDoc. # Use Postiz from your MCP Client Source: https://speakeasy.com/mcp/using-mcp/mcp-server-providers/postiz [Postiz](https://postiz.com/) connects to multiple social media platforms and schedules posts across channels. This guide shows you how to install the Postiz MCP server in Claude Desktop and use Claude to generate and schedule content for [X](https://x.com/), [LinkedIn](https://linkedin.com/), and other platforms. ## Prequisites You need: - [Claude Desktop](https://claude.ai/download) installed - A [Postiz account](https://platform.postiz.com/auth) ## Install the Postiz MCP server To connect Postiz to Claude Desktop, you first need to: - Connect at least one social media channel in Postiz - Get your MCP server URL from the Postiz settings ### Connect a social channel Click **Add Channel** in your Postiz dashboard to connect a social media account. ![Adding social media channel](/assets/mcp/using-mcp/social-media-postiz/postiz-add-channel-button.png) Postiz displays the social media options you can connect to. For this guide, use X and LinkedIn. ![Social media options](/assets/mcp/using-mcp/social-media-postiz/postiz-social-media-options.png) ### Retrieve the MCP server link From the Postiz dashboard, navigate to the **Settings** page and open the **Public API** tab. Find and copy the MCP server URL in the **MCP** section. This URL provides direct access to Postiz's posting and scheduling functionality using MCP. ![Copying MCP SSE link](/assets/mcp/using-mcp/social-media-postiz/postiz-mcp-config.png) In Claude Desktop, go to **Settings → Connectors** and click **Add Custom Connector**. Enter `Postiz` as the connector name and paste the Postiz MCP server URL in the designated field. ![Adding a Custom Connector](/assets/mcp/using-mcp/social-media-postiz/postiz-adding-postiz-mcp-to-claude.png) Click **Add** to save the connector and restart Claude Desktop. ## Test the integration Test the integration by asking Claude to generate two short posts and a longer, more detailed post about MCP tools in software engineering. The short posts work well for X, while the longer post suits LinkedIn better. Here's the prompt: ```txt Hi Claude! Draft two short X posts and one longer LinkedIn post about why MCP tools are essential for modern software engineering, then use the Postiz MCP server to schedule and publish them across both platforms. ``` Claude drafts the posts and verifies your Postiz configuration. ![Verifying Postiz config](/assets/mcp/using-mcp/social-media-postiz/postiz-config-verification.png) Then, Claude schedules the posts. Click the generated links to view your scheduled posts in Postiz. ![Viewing a scheduled post](/assets/mcp/using-mcp/social-media-postiz/postiz-scheduled-post-view.png) ## Conclusion With Postiz connected to Claude Desktop, you're ready for AI-powered social media scheduling. # Use Resend from your MCP Client Source: https://speakeasy.com/mcp/using-mcp/mcp-server-providers/resend This guide shows you how to connect the Resend MCP Server to Claude Desktop. ![Screenshot of the Claude Desktop UI showing an email was successfully sent](/assets/mcp/using-mcp/resend-guide/send-email-example.png) ## Prerequisites - A [Resend account](https://resend.com/) - [Claude Desktop](https://claude.ai/download) - [Node.js](https://nodejs.org/en/download/) installed on your system ## Retrieving the Resend API key The Resend MCP Server needs your Resend API key to authenticate with your account. In the Resend dashboard, click **API Keys** in the side bar. Then click **Create API Key**. ![Screenshot of the Resend UI showing the navigation path: API Keys → Create API Key](/assets/mcp/using-mcp/resend-guide/create-api-key-button.png) In the **Add API Key** dialog, enter a name for the API key, and leave the **Permission** and **Domain** fields at their defaults. The **Permission** and **Domain** settings control sending limits and domain restrictions, and the defaults allow unlimited sending from any domain, which works for development. In production, you should restrict these for security. Click **Add**. ![Screenshot of the "Add API Key" dialog in the Resend UI](/assets/mcp/using-mcp/resend-guide/api-key-form.png) Resend will display the API key. Copy and save it to use in the MCP server configuration in Claude Desktop. ![Screenshot of the "View API Key" modal in the Resend UI](/assets/mcp/using-mcp/resend-guide/copy-api-key.png) ## Cloning the Resend MCP project Clone the Resend MCP project: ```bash git clone https://github.com/resend/mcp-send-email.git cd mcp-send-email ``` Build the project: ```bash npm install npm run build ``` The `build` command generates an `index.js` file in the `build` directory. The build compiles TypeScript source code into JavaScript that Claude Desktop can execute, and bundles dependencies so the MCP server runs standalone. Copy the absolute path to the `index.js` file. You'll need it for the Claude Desktop configuration. ## Connecting the Resend MCP Server Now add the Resend MCP Server to the Claude Desktop configuration. In Claude Desktop, go to **Settings** → **Developer** → **Edit Config**. Add the Resend configuration to the `claude_desktop_config.json` file that opens: ```json { "mcpServers": { "resend": { "command": "node", "args": ["ABSOLUTE_PATH_TO_MCP_SEND_EMAIL_PROJECT/build/index.js"], "env": { "RESEND_API_KEY": "YOUR_RESEND_API_KEY" } } } } ``` - Replace `ABSOLUTE_PATH_TO_MCP_SEND_EMAIL_PROJECT/build/index.js` with the path to your project location. - Replace `YOUR_RESEND_API_KEY` with your Resend API key. Restart Claude Desktop. ## Testing the connection Test the connection by asking Claude to list the current audiences in the Resend: ```txt Please use the Resend MCP Server to list the current audiences in my Resend account ``` ![Screenshot of the Claude Desktop UI showing the list of audiences in the Resend application](/assets/mcp/using-mcp/resend-guide/list-audiences.png) ## Sending an email Now you can ask Claude to send an email to a specific audience or email address, for example: ```txt Please send a short welcoming email to the user example@email.com from onboarding@acme.dev. ``` ![Screenshot of the Claude Desktop UI showing an email was successfully sent](/assets/mcp/using-mcp/resend-guide/send-email-example.png) # Use Slack from your MCP Client Source: https://speakeasy.com/mcp/using-mcp/mcp-server-providers/slack This guide demonstrates how to connect your Slack workspace to your MCP Client. As Slack doesn't provide an official MCP server, and Anthropic's reference implementation was deprecated due to [security vulnerabilities](https://embracethered.com/blog/posts/2025/security-advisory-anthropic-slack-mcp-server-data-leakage/), this guide uses the open-source [`slack-mcp-server`](https://github.com/korotovsky/slack-mcp-server) instead. After following this guide you'll be able to read and summarize conversations, search message history, and analyze team communications without leaving Claude Desktop or whatever client you prefer. Here's an example where Claude finds all messages with reactions in a specific channel: ## Prerequisites To follow along, you'll need * A user account on a Slack workspace * An MCP Client like Claude Desktop * Ideally some familiarity with editing JSON files and using your browser's developer tools ## Getting your Slack `xoxc` and `xoxd` auth tokens Instead of connecting to the Slack API, which would require admin access, we'll use the browser session tokens to authorize the Slack MCP Server to connect to your Slack workspace. Your MCP client will then have access to anything that you would in Slack. To get these, you'll need to log into your Slack workspace using a browser like Google Chrome. To get the `xoxc` token: 1. Open the Slack workspace in your browser. 2. Open the developer console by pressing `Ctrl+Shift+I` (`Cmd+Option+I` on macOS) or `F12`. 3. Switch to the **Console** tab. 4. Type "allow pasting" into the console and press `Enter`. 5. Paste the following snippet into the console and press `Enter`: ```text JSON.parse(localStorage.localConfig_v2).teams[document.location.pathname.match(/^\/client\/([A-Z0-9]+)/)[1]].token ``` The token will be returned in the console. It starts with `xoxc-`. Save this somewhere. ![Browser developer console showing Slack xoxc token location](/assets/mcp/using-mcp/slack-claude-quickstart/slack-xoxc-token-console.png) To get the `xoxd` token: 1. Switch to the **Application** tab (**Storage** in Firefox and Safari). 2. In the sidebar, under **Storage**, click **Cookies**. 3. Find the cookie named `d` in the table. 4. Copy the cookie's value to the clipboard. ![Developer tools cookies panel showing how to locate and copy the Slack xoxd authentication token](/assets/mcp/using-mcp/slack-claude-quickstart/slack-xoxd-token-cookie.png) The token value starts with `xoxd-`. Save this somewhere. ## Cloning and modifying the project The Slack MCP Server only [recently added support](https://github.com/korotovsky/slack-mcp-server/pull/91) for reactions, and that functionality is not yet included in their latest release, so we need to clone and build the project locally. Run the following commands ```bash mkdir slack-mcp-setup cd slack-mcp-setup git clone https://github.com/korotovsky/slack-mcp-server.git cd slack-mcp-server go build -o slack-mcp-server ./cmd/slack-mcp-server ``` The build will be located in the root of the cloned project at `slack-mcp-server/slack-mcp-server`. ## Installing the MCP server in Claude Now update your Claude Desktop configuration to include the MCP server. Open Claude Desktop and go to **Settings** > **Developer** > **Edit Config**. ![Claude Desktop configuration settings](/assets/mcp/using-mcp/slack-claude-quickstart/claude-desktop-config-settings.png) In the `claude_desktop_config.json` file that opens, add the Slack MCP Server configuration: ```json { "mcpServers": { "SlackMCPServer": { "command": "PATH_TO_MCP_SERVER/slack-mcp-server", "args": ["-transport", "stdio"], "env": { "SLACK_MCP_XOXC_TOKEN": "YOUR_XOXC_TOKEN", "SLACK_MCP_XOXD_TOKEN": "YOUR_XOXD_TOKEN", "SLACK_MCP_USERS_CACHE": "PATH_TO_MCP_SERVER/.users_cache.json", "SLACK_MCP_CHANNELS_CACHE": "PATH_TO_MCP_SERVER/.channels_cache.json" } } } } ``` Replace `PATH_TO_MCP_SERVER` with the absolute path to your cloned `slack-mcp-server` directory. Replace `YOUR_XOXC_TOKEN` and `YOUR_XOXD_TOKEN` with the Slack tokens you saved previously. Restart Claude Desktop to load the server. To test that the connection is working, ask Claude to list the current channels in your Slack workspace. ## Testing reaction functionality In Claude Desktop, start a new chat. Click the **Search and tools** button to see the MCP server listed. Enable all tools if needed. ![Screenshot showing how to locate the Slack MCP Server in Claude Desktop "Search and tools"](/assets/mcp/using-mcp/slack-claude-quickstart/claude-mcp-server-settings.png) Add reaction emojis to messages in your Slack workspace, then ask Claude to show you messages with reactions to test the server's reaction-reading functionality. ![Slack messages with reactions displayed in Claude](/assets/mcp/using-mcp/slack-claude-quickstart/slack-messages-with-reactions.png) ## Conclusion Claude Desktop can now access your Slack workspace and read message reactions through the customized MCP server. While the server can be installed directly using a [DXT file](https://github.com/korotovsky/slack-mcp-server/blob/master/docs/03-configuration-and-usage.md#Using-DXT), building from source lets you customize functionality for your workflows. # Use Zendesk from your MCP Client Source: https://speakeasy.com/mcp/using-mcp/mcp-server-providers/zendesk This guide demonstrates how to connect your Zendesk workspace to Claude Desktop using the Model Context Protocol (MCP). Once connected, you can analyze support tickets, track customer satisfaction trends, and generate response templates without leaving the Claude interface. As Zendesk doesn't provide an official MCP server, this guide uses the open-source [`zendesk-mcp-server`](https://github.com/reminia/zendesk-mcp-server) that bridges your Zendesk workspace and Claude Desktop. ![Zendesk MCP architecture](/assets/mcp/using-mcp/zendesk/zendesk-mcp-architecture.png) Here's what the connection looks like in action: ## Prerequisites Before we begin, make sure you have: - [Claude Desktop](https://claude.ai/download) installed - A [Zendesk account](https://www.zendesk.com/) with admin access or API permissions - The [uv](https://docs.astral.sh/uv/) Python package manager ## Setting up a Zendesk application In your Zendesk dashboard, click the **Settings** icon in the sidebar. ![Navigating to settings](/assets/mcp/using-mcp/zendesk/zendesk-settings-navigation.png) On the Settings page, click **Apps and integrations**. Then, click **API tokens** in the left navigation menu and click the **Add API token** button. ![Click on Add API token](/assets/mcp/using-mcp/zendesk/zendesk-add-api-token.png) Zendesk then displays the API token creation form. Enter `Zendesk MCP token` in the **Description** field and click **Save**. ![Copy API token](/assets/mcp/using-mcp/zendesk/zendesk-copy-token.png) Copy the generated token immediately and save it securely. You won't be able to view this token again. You'll need this token to configure the Claude Desktop connection in the next step. ![API token created](/assets/mcp/using-mcp/zendesk/zendesk-api-token-created.png) To make sure you can make API requests to the Zendesk API with the token, navigate to the **API configuration** page and select the **Allow API token access** option displayed there. ![Claude Desktop configuration](/assets/mcp/using-mcp/zendesk/allow-api.png) ## Installing the Zendesk MCP server in Claude To install the Zendesk MCP server, you need uv installed on your machine. **For macOS users (recommended):** ```bash # Install with Homebrew (ensures Claude Desktop can find uv) brew install uv ``` **For other platforms:** ```bash # For Linux curl -LsSf https://astral.sh/uv/install.sh | sh # For Windows powershell -ExecutionPolicy ByPass -c "irm https://astral.sh/uv/install.ps1 | iex" ``` **Note:** If you install uv using the script method and Claude Desktop can't find the `uv` command, you may need to use the full path in your configuration: `/Users/YOUR_USERNAME/.local/bin/uv` instead of just `uv`. Once it's done, clone the `zendesk-mcp-server` project. ```bash git clone https://github.com/reminia/zendesk-mcp-server.git ``` Then, navigate to the newly cloned project. ```bash cd zendesk-mcp-server ``` Install the project dependencies and the project itself: ```bash uv sync uv pip install -e . ``` Inside the project, create a file named `.env`. Add your Zendesk credentials to this file: - `ZENDESK_SUBDOMAIN`: Add the subdomain part of your Zendesk URL. For example, if your Zendesk URL is `https://mycompany-123.zendesk.com`, then use `mycompany-123`. - `ZENDESK_EMAIL`: Add the email address associated with the account you used to create the API token. - `ZENDESK_API_KEY` : Add the API token you created earlier. The file should look like this: ```txt ZENDESK_SUBDOMAIN=xxx ZENDESK_EMAIL=xxx ZENDESK_API_KEY=xxx ``` Now, update your Claude Desktop configuration to include the MCP server. In **Settings**, go to **Developer** > **Edit Config**. ![Claude Desktop configuration](/assets/mcp/using-mcp/zendesk/claude-desktop-config-settings.png) In the `claude_desktop_config.json` file that opens, add the Zendesk MCP Server configuration: ```json { "mcpServers": { "zendesk": { "command": "uv", "args": [ "--directory", "PATH_TO_MCP_SERVER/zendesk-mcp-server", "run", "zendesk" ] } } } ``` Replace `PATH_TO_MCP_SERVER` with the absolute path to your cloned `zendesk-mcp-server` directory. Restart Claude Desktop to load the server. ## Testing the connection Test that the Zendesk MCP server has been installed correctly by entering the following prompt: ```txt Hi Claude. Please list the current tickets I have on Zendesk. ``` You should receive a similar reply to the following: ![Claude showing current tickets](/assets/mcp/using-mcp/zendesk/claude-showing-tickets.png) Then, ask Claude to add comments to the tickets to show they're being addressed. For example: ```txt Please add comments to these tickets to indicate that they are currently being addressed. ``` ![Claude adding comment to Zendesk ticket](/assets/mcp/using-mcp/zendesk/claude-adding-comment.png) You should see the comment added in your Zendesk dashboard. ![Showing added comment](/assets/mcp/using-mcp/zendesk/zendesk-comment-added.png) ## Conclusion Now that you can access your Zendesk workspace through Claude Desktop, try combining this functionality with other MCP servers. For example, you could use the [Slack integration](/mcp/using-mcp/mcp-server-providers/slack) to automatically notify your support team about urgent tickets, or connect with your CRM to update customer records based on ticket resolutions. # Popular MCP use cases Source: https://speakeasy.com/mcp/using-mcp/use-cases MCP enables AI agents to interact with virtually any system or service. Here are some popular ways you can use MCP today. ## MCP for software development You know how, some days, you spend more time switching between GitHub issues, pull requests, and documentation than actually writing code? The [GitHub MCP Server](https://github.com/github/github-mcp-server) can help you delegate these tasks to your AI coding assistant. ### Managing pull requests using the GitHub MCP Server For example, here we use the GitHub MCP Server to check the status of a recent pull request: > Get the status of my Context7 PR. Here, we see Claude searching for the pull request and responding with the status. ### Deleting a local branch using Desktop Commander Next, let's use the [Desktop Commander](https://desktopcommander.app/) MCP server to delete the local branch associated with that pull request and do some housekeeping: > Delete the local branch for this PR in ~/projects/personal/mcp-servers/context7 and rename local master to main. Here, we see Claude deleting the local branch and renaming the `master` branch `main`. How's that for convenience? Sure, you could have done `gh pr status` and `git branch -d context7` yourself, but with MCP, your AI assistant could decide to do this for you, or even suggest it proactively based on your recent activity. This is just one example of how MCP can streamline your software development workflow. ### Finding documentation using Context7 Context7 is one of the most popular MCP servers for software development. It can help your AI coding assistant find the most recent documentation and code examples for more than 21,000 libraries and frameworks. This helps fill in the knowledge gaps that LLMs have due to training cutoffs. Context7 can even help your assistant find code snippets for specific, older versions of libraries. This greatly reduces hallucinations. Let's see how it works in practice. We'll ask Claude for help developing an MCP server: > Show me how to elicit user input using the MCP TypeScript SDK. Use Context7. Claude uses the Context7 MCP server to find the latest documentation and code samples for the MCP Specification. It then generates TypeScript code samples for an MCP server and an MCP client. ## MCP for number crunching Despite recent advances, we all know LLMs still struggle with math and calculations. Especially when working with large numbers, the results can be wildly inaccurate. ### Using MCP Run Python for accurate calculations With [MCP Run Python](https://github.com/pydantic/mcp-run-python), we can offload any calculations to a Python environment. Let's ask Claude to help us with a tricky math problem: > I want to see the first moon landing live on TV. If I leave tomorrow at 6am PST, calculate the exact number of seconds I need to go back to see the touchdown three minutes after I arrive. (I need this for my time machine, obviously.) Calculate the number manually, then do the calculation again using Python. Compare the results. Claude first tries the calculation manually, then uses the MCP Run Python server to do the calculation again. The results show that **the manual calculation was off by about 53.2 days**! Imagine traveling 56 years back in time, only to find out you need to wait two months to see the moon landing. To avoid such mistakes, we recommend adding a Python server to your MCP setup, then adding the following to your system prompt or rules: > If you need to do any calculations, always use the `run_python_code` tool to ensure accuracy. ## MCP for browser automation Giving an LLM access to a web browser is something to behold. It can search, navigate, take screenshots, and even fill out forms. ### Browser automation using Playwright MCP We'll use the [Playwright MCP server](https://github.com/microsoft/playwright-mcp) to automate browser tasks. This is particularly useful for testing web applications, scraping data, or even just finding information online. So let's ask Claude to open a browser and help us with comparative shopping: > Find three cheap aluminium 60% mechanical keyboards in beige or grey on AliExpress. Claude opens the browser, searches for the keyboards, and finds three options. Your AI coding assistant can use the Playwright MCP server to automate testing or take screenshots of your site while it tries to fix CSS issues. The Playwright server can also be used to automate any web-based tasks, like filling out forms or scraping data. ## Getting started with these use cases You can use these examples in Claude Desktop by adding the MCP servers to your `claude_desktop_config.json` file: ```json { "mcpServers": { "context7": { "command": "npx", "args": ["-y", "mcp-remote@latest", "https://mcp.context7.com/sse"] }, "run-python": { "command": "docker", "args": [ "run", "-i", "--rm", "denoland/deno", "run", "-N", "-R=node_modules", "-W=node_modules", "--node-modules-dir=auto", "jsr:@pydantic/mcp-run-python", "stdio" ] }, "playwright": { "command": "npx", "args": ["-y", "@playwright/mcp@latest"] }, "desktop-commander": { "command": "npx", "args": ["-y", "@wonderwhy-er/desktop-commander@0.2.3"] }, "githublocal": { "command": "docker", "args": [ "run", "-i", "--rm", "-e", "GITHUB_PERSONAL_ACCESS_TOKEN", "-e", "GITHUB_TOOLSETS=all", "ghcr.io/github/github-mcp-server" ], "env": { "GITHUB_PERSONAL_ACCESS_TOKEN": "ghp_XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX" } } } } ``` ## Using MCP with different AI agents In this guide, we focused on using MCP with Claude, but you can use MCP with any AI agent that supports MCP. You can find a list of MCP clients and agents on the [MCP website](https://modelcontextprotocol.io/clients). ## Real-world MCP: How companies are using MCP in production today When Anthropic released the [Model Context Protocol (MCP)](https://modelcontextprotocol.io/introduction) in November 2024, the usual suspects wasted no time experimenting. But here's the thing: Instead of being stuck as a demo on GitHub, MCP quietly started showing up in real-world production systems. To understand the real impact of MCP, we investigated how companies are implementing it in production, from [Zed](https://zed.dev/) building its Agent Panel around MCP from day one, to [Solana](https://solana.com/) developers using MCP to manage DeFi protocols through conversational interfaces, [Stripe](https://stripe.com/) using MCP to monetize AI tools, and more. Here's what we learned. ### Block's MCP implementation Between [Cash App](https://cash.app/) and [Square](https://squareup.com/), [Block](https://block.xyz/) handles [billions of dollars in payments](https://www.businessofapps.com/data/cash-app-statistics/) and processes transactions for [over four million merchants](https://squareup.com/us/en/about#:~:text=More%20than%20four%20million%20sellers,and%20the%20largest%20international%20chains.). Block's AI agent [goose](https://github.com/block/goose) (which uses [Claude in Databricks](https://www.anthropic.com/customers/block) as its default model) started out as a coding assistant. Now that goose can use MCP to connect to Block's internal systems, the tool powers work across the entire company. About 4,000 of Block's 10,000 employees actively use goose across 15 different job roles, from sales and design to customer success and operations. The MCP-enabled system democratizes data access at Block: Employees who don't know SQL can solve their own data problems by describing what they need in plain English. Security analysts create detection rules by describing threats naturally instead of wrestling with complex query syntax. Block developers can build an MCP server for any tool and instantly make it available to AI agents. As the setup uses OAuth with short-lived credentials, employees don't need to manage API keys. Three-quarters of Block's engineers report saving 8-10 hours per week using goose. The tool has become so effective that [one engineer says](https://www.anthropic.com/customers/block#:~:text=Axen%20said%2C%20%22goose,become%20that%20effective.%22), "90% of my lines of code are now written by goose." ### Zed makes MCP feel native The team behind performance-focused code editor [Zed](https://zed.dev/) built its Agent Panel around MCP from the start, rather than adding AI features to an existing architecture. Take Zed's database integration. You can type `/pg-schema users` and get the schema for your users table instantly. ![Zed Postgres](/assets/mcp/real-world-mcp-usage/zed-postgres.png) But it gets more interesting with Zed's Neon integration, which lets you safely modify production databases: ``` User: "Can you add a created_at column to the table?" → Agent runs prepare_database_migration → Creates temporary branch for safe schema changes → User confirms with "yes, do it" → Agent runs complete_database_migration → Production schema updated safely ``` That's the kind of workflow that would be nerve-wracking without proper guardrails, but MCP's stateful sessions make it possible to build safely. The technical implementation is clean: MCP servers run as separate processes that communicate with Zed's Agent Panel via `stdio`, with support for HTTP and server-sent events coming soon. Extension developers can register servers in their `extension.toml` and implement a simple `context_server_command` method. ### Replit's MCP playground [Replit](https://replit.com/) is a cloud-based development environment platform that handles setup and hosting for coding projects. It supports multiple programming languages and lets you run code directly in the browser without any local installations. Recently, Replit pivoted to agent-powered coding, positioning itself as an AI-first development platform for vibe coding entire applications with zero setup. Replit's MCP templates come with pre-configured servers, so you can start a new environment and have working AI integrations without installing or configuring anything yourself. The Learn About MCP template includes YouTube processing, filesystem access, and external API integration out of the box. The configuration lives in a simple JSON file: ```json { "systemPrompt": "You are an AI assistant helping a software engineer", "llm": { "provider": "openai", "model": "gpt-4o-mini", "temperature": 0.2 }, "mcpServers": { "fetch": { "command": "uvx", "args": ["mcp-server-fetch"], "requires_confirmation": ["fetch"] }, "youtube": { "command": "uvx", "args": [ "--from", "git+https://github.com/adhikasp/mcp-youtube", "mcp-youtube" ] }, "filesystem": { "command": "npx", "args": ["-y", "@modelcontextprotocol/server-filesystem", "./"] } } } ``` What makes this compelling is that you can type something like "Summarize this video https://youtu.be/1qxOcsj1TAg and write the summary to summary.txt" and watch it happen automatically. The AI coordinates between multiple services, fetching the video transcript, processing it, and saving the output – all through standardized MCP interfaces. Replit Agent takes this multi-service coordination further by automatically integrating services like EmailJS and Mailtrap when you describe what you want to build. Ask for a React app with email functionality, and the agent configures the MCP servers, sets up the integrations, and builds the app. It's the kind of seamless experience that makes MCP's complexity worthwhile. ### Code intelligence platforms embrace MCP [Sourcegraph](https://sourcegraph.com/) and [Codeium](https://codeium.com/) both see MCP as a way to differentiate their code intelligence platforms. The implementations are telling, showing how established companies are using MCP not just for novelty, but for genuine competitive advantage. ![Code intelligence diagram](/assets/mcp/real-world-mcp-usage/code-intelligence-diagram.png) Sourcegraph implements MCP through [OpenCtx](https://openctx.org/) (Sourcegraph's own standard for external context), with [Cody](https://sourcegraph.com/cody) acting as the MCP client. Sourcegraph also has a [batch changes MCP server](https://github.com/sourcegraph/test-mcp) that automates large-scale modifications across repositories, and a React Props server that helps developers understand component usage patterns across entire codebases. The workflow feels natural: Cody can analyze your database schema to suggest query optimizations, pull GitHub issues directly into your editor context, or search across multiple repositories with semantic understanding. It's code intelligence that actually understands the broader context of your work. Codeium took a different approach with the [Windsurf](https://codeium.com/windsurf) IDE. Windsurf Wave 3 includes MCP support, allowing [Cascade](https://codeium.com/blog/introducing-cascade) to connect to multiple servers simultaneously. The configuration is straightforward: ```json { "mcpServers": { "github": { "command": "npx", "args": ["-y", "@modelcontextprotocol/server-github"], "env": { "GITHUB_PERSONAL_ACCESS_TOKEN": "your_token_here" } }, "postgres": { "command": "docker", "args": ["run", "--rm", "-i", "--env-file", ".env", "mcp-postgres"] } } } ``` Windsurf also features UI panels for managing MCP servers, one-click setup for popular servers, and proper sandboxing. It's the kind of polish that suggests MCP integration is becoming table stakes for development tools. ### Solana blockchain data access Several developers have built MCP servers that make Solana blockchain data accessible to AI assistants. While you can't execute transactions, these tools turn complex blockchain queries into simple conversations. The most documented implementation comes from [QuickNode's comprehensive tutorial](https://www.quicknode.com/guides/ai/solana-mcp-server). This MCP server handles some typical blockchain analysis tasks: checking wallet balances, retrieving token accounts, examining transaction details, and querying account information. What makes it practical is the integration with [Solana Agent Kit](https://github.com/sendaifun/solana-agent-kit), which handles all the RPC complexity behind a clean interface. The [Solscan MCP](https://mcp.so/server/solscan-mcp/Valennmg) server takes a different approach. It's built on [Solscan's](https://solscan.io/) API infrastructure, giving you access to richer data analysis capabilities, like token metadata (names, symbols, logos), market data from various exchanges, holder distribution analytics, and detailed DeFi activity tracking. The server can tell what tokens a wallet holds, how those holdings have changed over time, and which DeFi protocols the wallet has interacted with. Both Solana MCP implementations focus on data access rather than transaction creation. You can ask an AI assistant to "analyze this wallet's DeFi activity" or "explain what happened in this transaction" and get useful answers without any security risks. The read-only design means you get the benefits of natural language blockchain analysis without worrying about accidental transactions or compromised keys. For most blockchain analysis use cases, this is exactly what you want: insight without risk. ### Stripe introduces paid MCP servers The [Stripe Agent Toolkit](https://github.com/stripe/agent-toolkit/) lets developers create MCP servers that charge for tool usage. The toolkit runs on Cloudflare Workers and handles OAuth authentication. When an AI agent calls a paid tool, the MCP server checks if the user has already paid. If not, it creates a Stripe checkout session and returns a payment URL. After payment, the tool becomes available. ![Stripe MCP monetization](/assets/mcp/real-world-mcp-usage/stripe-mcp-monetization.png) The system supports one-time payments, subscriptions, and usage-based billing through Stripe's metering API. Developers can charge per API call, search query, or processing job. ### The growing MCP ecosystem The MCP ecosystem has grown to over 15,000 community-built servers, creating powerful network effects. #### AWS ecosystem integration The hundreds of services available from AWS are notoriously complex to navigate. MCP servers simplify this by letting AI assistants work directly with AWS through standard interfaces. [Amazon Bedrock Samples](https://github.com/aws-samples/amazon-bedrock-samples) enables natural language queries to knowledge bases with vector database searches, RAG pipeline integration with automatic document chunking, and multi-modal retrieval for text, images, and structured data. The [AWS Cloud Development Kit (CDK)](https://github.com/aws/aws-cdk) generates infrastructure code from natural language descriptions, creating CloudFormation templates with validation and resource dependency mapping that includes cost estimation. **Cost Analysis** tools decode AWS billing complexity through usage analysis for optimization recommendations, CloudWatch-based rightsizing suggestions, and cost forecasting with budget alerts. #### Database platform innovations MCP bridges conversational AI with complex data operations, letting developers work with sophisticated databases using natural language instead of learning new APIs. [DataStax Astra DB](https://www.datastax.com/products/datastax-astra) combines NoSQL with vector search for AI applications, handling vector search for knowledge bases, document CRUD operations with schema validation, collection management with automatic scaling, and real-time analytics with aggregation pipelines. The [ClickHouse MCP Server](https://github.com/ClickHouse/mcp-clickhouse) makes high-performance analytics accessible to non-specialists through time-series analysis for metrics and events, columnar storage optimization for analytical workloads, distributed query execution across cluster nodes, and real-time data ingestion with exactly-once processing. The server handles query optimization automatically, turning natural language questions into optimized SQL that processes billions of events in seconds. #### Creative tool integration Creative MCP implementations solve real workflow problems for artists, game developers, and content creators working with complex tools. [BlenderMCP](https://github.com/ahujasid/blender-mcp) by Siddharth Ahuja removes barriers between creative vision and technical execution. Instead of memorizing hundreds of keyboard shortcuts, users can focus on creativity through 3D scene generation from natural language descriptions, object manipulation with physics simulation, animation creation with keyframe interpolation, and rendering pipeline control with material and lighting setup. The server automatically configures rigid body dynamics, gravity, and collision detection when you say "make these objects fall realistically." [MCP Unity Editor](https://github.com/CoderGamester/mcp-unity) by Miguel Tomas streamlines game development workflows through game object creation and component management, scene manipulation with hierarchy management, script generation with C# code compilation, and asset pipeline integration with import and export workflows. The script generation understands Unity's patterns and conventions, producing properly structured C# code with appropriate component references rather than generic code that won't work in Unity's context. ### Why companies are choosing MCP Let's take a look at the compelling technical and business reasons companies are adopting MCP. #### The USB moment for AI tools Just as the [USB standard](https://en.wikipedia.org/wiki/USB) changed computing by replacing proprietary connectors, MCP standardizes how AI connects to tools. Without MCP, connecting an AI assistant to Zendesk, Salesforce, and Slack means building three separate integrations from scratch. Each integration needs custom authentication, error handling, and data formatting – like having a different port for every device. With MCP, you write one server implementation that exposes Zendesk's capabilities to any AI system that speaks the protocol. Instead of building N integrations for every M tools (N x M complexity), you only need to build N servers and M clients. This matters when you're dealing with dozens of internal tools and multiple AI agents. #### Inherent security MCP creates a single chokepoint for AI access to company systems. Instead of tracking permissions across dozens of custom integrations, connections flow through MCP servers that can be monitored, logged, and controlled consistently. For example, [Gram's implementation of MCP](/docs/gram/concepts/environments) allows for environment-specific tool access and the ability to define different credentials and server URLs for different environments. With Gram, you can create separate `staging` and `production` environments, each with its own credentials and server URL tailored to different access requirements. #### Switching models without rebuilding from scratch Since MCP servers are model-agnostic, companies can experiment with different AI providers without throwing away their tool integrations. This flexibility helps with avoiding vendor lock-in, testing new models for specific tasks, and lowering costs as inference pricing comes down. ## Exploring more MCP servers and clients If you're considering MCP for your organization, [MCP.so](https://mcp.so/) has an extensive list of MCP servers and clients, including MCP servers that may integrate with your existing tools. To learn more about MCP and how to try it out, check out our [quickstart guide to installing MCP servers](/mcp/getting-started/quickstart). # Account management with MCP (Slack + Hubspot) Source: https://speakeasy.com/mcp/using-mcp/use-cases/account-management Account management typically involves tracking data across multiple tools. Maybe you discuss potential deals on Slack and then to update leads on Hubspot with that information. This guide shows you how to automatically update your HubSpot contacts based on Slack messages and emoji reactions. As an example, we'll add a note to the relevant contact on Hubspot if someone adds a ‼️ emoji to a message on Slack that mentions that person, but you can adapt the exact steps to your own workflow. ## Prerequisites - Slack and HubSpot MCP servers installed. Follow these guides to connect each service individually. - [MCP Use Case: Connect Slack](/mcp/using-mcp/mcp-server-providers/slack) - [MCP Use Case: Connect HubSpot](/mcp/using-mcp/mcp-server-providers/hubspot) - An MCP client like [Claude Desktop](https://claude.ai/download) ## Using Slack and Hubspot together with MCP Let's imagine that your team shares lead updates in a Slack channel called `#leads` and marks urgent ones with the ‼️ emoji. You want Claude to add a "Schedule call ASAP" note to those contacts in HubSpot. Ask Claude: ```txt In the #leads Slack channel, find the messages marked with :bangbang: emoji reactions. Use the HubSpot tool to add a note to those contacts that I should call them ASAP. ``` ![Claude Desktop processing Slack messages with emoji reactions](/assets/mcp/using-mcp/account-management/claude-processing-slack-messages.png) Claude will: 1. Search the #leads channel for messages with ‼️ reactions. 2. Parse contact information from those messages. 3. Look up matching contacts in HubSpot. 4. Add a priority note to each contact record. ![Claude Desktop showing workflow execution results](/assets/mcp/using-mcp/account-management/claude-workflow-execution-result.png) The contacts in HubSpot will now have a "schedule call ASAP" note added to their records. ![HubSpot contact record with updated notes](/assets/mcp/using-mcp/account-management/hubspot-contact-record-updated.png) ## Best practices for using multiple MCP servers - **Be specific about which tool to use:** "Use HubSpot to get contact details for John Smith" is better than saying "Get the contact info for John Smith." - **Design prompts like workflows:** If the steps make sense to you, they'll make sense to Claude. Break complex tasks into clear, sequential actions. - **Test individual servers first:** Verify each MCP server works independently before combining them in complex workflows. Combined MCP servers turn Claude Desktop into a powerful automation hub. The key is writing clear prompts that specify which services to use in what order. # Customer support with MCP (Zendesk + Slack + internal licensing API) Source: https://speakeasy.com/mcp/using-mcp/use-cases/customer-support Customer support teams switch between tools constantly: checking Slack for context about customer issues, finding tickets in Zendesk, adding comments, and then handling the issues in internal dashboards or waiting for dedicated teams to handle those issues. For customer support teams handling dozens of tickets daily, more time is spent context-switching and navigating interfaces when this time could be used to handle more customer complaints. What if you could help more customers faster without losing context or time? There's a simpler approach: Use MCP servers to connect all your tools. Instead of switching between Slack, Zendesk, and your internal systems, handle everything through Claude conversations. This guide shows you how to connect Zendesk, Slack, and your internal license API through MCP servers, so customer support agents can resolve tickets in minutes. We'll show a basic internal licensing API as an example, but you can switch this out for any internal API you need your team to interact with. ## What are we building? We're building a customer support workflow that connects Zendesk, Slack, and an internal License API through MCP servers. The customer support agents can ask Claude to find the customer's ticket in Zendesk, check related Slack discussions, and provision the license through the internal API. You'll set up three MCP servers: - A Zendesk MCP server to find tickets and add comments - A Slack MCP server for finding context about customer issues - An internal API MCP server for provisioning licenses ![Customer support workflow diagram](/assets/mcp/using-mcp/customer-support/customer-service-workflow-diagram.png) ## Prerequisites To follow this quickstart, you need: - [Claude Desktop](https://claude.ai/download) installed - Access to [Zendesk](https://www.zendesk.com/) and Slack accounts - An internal API with OpenAPI documentation (or a copy of the [License API](https://github.com/speakeasy-api/examples/tree/main/taskmaster-internal-api)) ## Setting up Zendesk and Slack Follow these guides to set up the first two MCP servers: - [Using Slack with MCP](/mcp/using-mcp/mcp-server-providers/slack) - [Using Zendesk with MCP](/mcp/using-mcp/mcp-server-providers/zendesk) Once you've added both servers to your `claude_desktop_config.json` and restarted Claude Desktop, test the setup. ### Testing the flow Open Claude Desktop and enter the following prompt. ```txt Find recent support tickets about license issues and check if there are any discussions about them in our #support Slack channel. ``` Claude will search both systems and present the information. At this point, the customer support agents have the customer's details and the context about their issue. ![Claude searches Zendesk and Slack](/assets/mcp/using-mcp/customer-support/claude-searches-zendesk-slack.png) The Zendesk and Slack integrations already save time. Instead of customer support agents having to go through Zendesk and scroll through Slack channels, Claude gathers everything in seconds. But it's likely that the customer support agents still have to switch to another system to handle the license issue. You can automate this final step by creating an MCP server for the internal License API. If you have an OpenAPI document for your API, Gram can generate the MCP server in minutes. ## Create the License MCP server Now, let's automate the manual license creation step by creating an MCP server for the License API. ### Install and run the Taskmaster License API If you don't have an internal API ready, clone the License API to follow along: ```bash git clone https://github.com/speakeasy-api/examples.git cd examples/taskmaster-internal-api chmod +x setup.sh ./setup.sh ``` The API runs at `http://127.0.0.1:8000` and includes the OpenAPI documentation. In production, you would use your actual internal tool API instead. ### Create the MCP server on Gram Usually, building an MCP server manually requires writing code with the MCP SDK, TypeScript, or FastMCP, then managing the hosting infrastructure. We'll use [Gram](https://getgram.ai) to generate an MCP servers from OpenAPI the existing OpenAPI documents and handle the hosting on its free tier. [Sign up for Gram](https://getgram.ai) and follow these steps: - On the [**Toolsets** page](/docs/gram/build-mcp/create-default-toolset), upload the License API's OpenAPI document. - Create a toolset named `License` and enable the tools you need. ![Enable all tools](/assets/mcp/using-mcp/customer-support/gram-enable-all-tools.png) - In the [**Auth** tab](/docs/gram/concepts/environments), set `LICENSE_API_SERVER_URL` to the URL of your internal tool API. If you're following this guide with the local License API, expose the API with [ngrok](https://ngrok.com/) by running the `ngrok http 127.0.0.1:8000` command and using the forwarding URL to fill in the `LICENSE_API_SERVER_URL` variable. - In **Settings**, create a [Gram API key](/docs/gram/concepts/api-keys). ### Install the MCP server in Claude Desktop In your License toolset's MCP tab, copy the **Managed Authentication** configuration. ![Use Managed authentication](/assets/mcp/using-mcp/customer-support/gram-managed-authentication-config.png) Open the Claude Desktop application, navigate to **Settings -> Developer**, and click **Edit Config**. ![Edit config](/assets/mcp/using-mcp/customer-support/claude-desktop-edit-config.png) Open the `claude_desktop_config.json` file, copy the **Managed Authentication** configuration from Gram, and replace `` with the value of the Gram API key you created. ```json { "mcpServers": { "GramLicense": { "command": "npx", "args": [ "mcp-remote", "https://app.getgram.ai/mcp/xxxxxlu", "--header", "Gram-Environment:default", "--header", "Authorization:${GRAM_KEY}" ], "env": { "GRAM_KEY": "Bearer ``" } } } } ``` Restart Claude Desktop, open a new chat, and verify that the integration works by asking Claude to create a license for a test user. ![Create a license in Claude Desktop](/assets/mcp/using-mcp/customer-support/claude-desktop-license-creation.png) ## The complete customer support workflow Your `claude_desktop_config.json` should look like this: ```json { "mcpServers": { "GramLicense": { "command": "npx", "args": [ "mcp-remote", "https://app.getgram.ai/mcp/", "--header", "Gram-Environment:default", "--header", "Authorization:${GRAM_KEY}" ], "env": { "GRAM_KEY": "Bearer " } }, "zendesk": { "command": "uv", "args": [ "--directory", "/path/to/zendesk-mcp-server", "run", "zendesk" ] }, "SlackMCPServer": { "command": "/path/to/slack-mcp-server", "args": [ "-transport", "stdio" ], "env": { "SLACK_MCP_XOXC_TOKEN": "YOUR_XOXC_TOKEN", "SLACK_MCP_XOXD_TOKEN": "YOUR_XOXD_TOKEN", "SLACK_MCP_USERS_CACHE": "PATH_TO_MCP_SERVER/.users_cache.json", "SLACK_MCP_CHANNELS_CACHE": "PATH_TO_MCP_SERVER/.channels_cache.json" } } } } ``` To test the complete workflow, open Claude Desktop and send the following prompt: ```txt Hi Claude. Find the most recent support ticket about license issues in Zendesk. Check if there are any related discussions in the #support Slack channel. Once you have the customer's information and context about their issue, create a new license for them using the License API and add a comment on the Zendesk tickets to confirm the fix. ``` The specific response will vary based on your actual data, but you'll observe Claude: - Finding the support ticket in Zendesk, checking related discussions in Slack, and gathering context about the customer's issue. ![Claude finds ticket and Slack context](/assets/mcp/using-mcp/customer-support/claude-finds-ticket-and-slack-context.png) - Using your new License MCP server to create licenses for the customer having issues, and adding a comment to the Zendesk ticket. ![Claude creates license and updates ticket](/assets/mcp/using-mcp/customer-support/claude-creates-license-and-updates-ticket.png) ## Final thoughts You've built a customer support workflow that connects Zendesk, Slack, and an internal License API through MCP servers. Gram hosted the internal API integration without requiring infrastructure management or custom MCP server code. The License API represents any internal service with an OpenAPI document. You can replace it with your existing systems to create workflows for: - **User provisioning:** Combine Slack discussions with Zendesk tickets to automatically create user accounts in your SaaS platform. - **Inventory management:** Use customer requests from support tickets to trigger inventory updates or purchase orders. - **Financial reporting:** Aggregate data from your support system, communication tools, and internal billing systems for automated reporting. # Social media scheduling with MCP (Slack + Postiz) Source: https://speakeasy.com/mcp/using-mcp/use-cases/social-content-creation If you're like most communicative teams, your Slack channels are full of great insights, customer feedback, and interesting discussions that never make it beyond your workspace. This guide demonstrates how to use the Slack and Postiz MCP servers with Claude Desktop to automatically transform those valuable conversations into social media content. ## Prerequisites Before you get started, make sure you have: - [Claude Desktop](https://claude.ai/download) installed - Both MCP servers set up: - [Slack MCP setup guide](/mcp/using-mcp/mcp-server-providers/slack) - [Postiz MCP setup guide](/mcp/using-mcp/mcp-server-providers/postiz) ## Installing the MCP servers After following both setup guides, your `claude_desktop_config.json` should include the Slack MCP server: ```json { "mcpServers": { "SlackMCPServer": { "command": "PATH_TO_MCP_SERVER/slack-mcp-server", "args": ["-transport", "stdio"], "env": { "SLACK_MCP_XOXC_TOKEN": "YOUR_XOXC_TOKEN", "SLACK_MCP_XOXD_TOKEN": "YOUR_XOXD_TOKEN", "SLACK_MCP_USERS_CACHE": "PATH_TO_MCP_SERVER/.users_cache.json", "SLACK_MCP_CHANNELS_CACHE": "PATH_TO_MCP_SERVER/.channels_cache.json" } } } } ``` You should also see your Postiz connector listed in Claude Desktop, under **Settings → Connectors**. ![Postiz connector](/assets/mcp/using-mcp/postiz-connector.png) With Slack and Postiz configured in Claude, you can test the integration. ## Testing the integration Here's a practical scenario: Let's say your team uses 🚀 emoji reactions to mark Slack threads that contain insights worth sharing externally. You can ask Claude to find these conversations and turn them into social media posts: Here is the prompt: ``` Find threads in the #threads channel that have 🚀 emoji reactions. Read those thread messages, analyze the key points, and create engaging X posts using the Postiz MCP server. ``` When you run this prompt, Claude searches your Slack workspace, analyzes the marked conversations, and creates optimized posts for your social platforms. ![Claude analysis](/assets/mcp/using-mcp/claude-analysis.png) Claude schedules the posts using Postiz, so you can review them at [platform.postiz.com](https://platform.postiz.com/). ![Claude using Postiz](/assets/mcp/using-mcp/postiz-scheduled-posts.png) ## Conclusion Clause can now automatically turn your best Slack threads into social media posts by finding threads marked with 🚀 reactions. Customize this workflow to suit your team's needs and skills. For example, telling Claude to focus on messages from specific channels or users may help it create content that resonates with your audience. # Best practices for using MCP tools Source: https://speakeasy.com/mcp/using-mcp/using-tools Model Context Protocol (MCP) tools are designed to be used by Large Language Models (LLMs), so why would we need to learn how to use them? We've learned that leaving tool selection over to the LLM alone can lead to wasted tokens, crammed context windows, and agents running in circles. To avoid these issues, let's explore how to make the most of MCP tools. If you haven't had great success using MCP before, we're not here to say, "You're holding it wrong." We're mostly saying that when it comes to tool selection, less is more. ## MCP tool selection: Add fewer tools When adding MCP servers to your client, you may be tempted to give the LLM access to every single tool you can find. This would be a mistake. Overwhelming the LLM with tool choices often leads to suboptimal tool choices or wasted tokens while the LLM goes on irrelevant side quests. Some clients acknowledge this problem by showing the user a warning when the number of tools exceeds a certain threshold. ![A screenshot of the Cursor Settings interface shows a warning about exceeding the total tools limit, which is quoted below.](/assets/mcp/using-tools/cursor-limit.png) Here, we see that Cursor shows a warning when the number of tools from installed MCP servers exceeds 40: > Exceeding total tools limit. You have 56 tools from enabled servers. Too many tools can degrade performance, and some models may not respect more than 40 tools. Perhaps the Cursor developers chose 40 after some experimentation, but [research shows](https://arxiv.org/abs/2411.15399) that some smaller models get confused long before 40 tools are included in the context. The problem does not appear to be caused by the context window filling up (due to too many tool definitions using too many tokens). Instead, at least for smaller or quantized models, the issue seems to stem from confusion. The LLM gets tools' names and definitions mixed up, hallucinates tools, or doesn't follow the instructions in tool descriptions. How do we help an LLM in situations like this? ## Tool loadout: Limit MCP tools per conversation > Tool Loadout is the act of selecting only relevant tool definitions to add to your context. In his excellent blog post, [How to Fix Your Context](https://www.dbreunig.com/2025/06/26/how-to-fix-your-context.html#tool-loadout), Drew Breunig shares strategies for improving performance through tool loadout. While some of these strategies recommend using retrieval-augmented generation (RAG) to help an LLM select the most relevant tools when it's faced with too many options, there is a much simpler solution for most clients: Deactivate the tools you know the LLM won't need. When you're starting a task, check whether the client you're using supports enabling selected MCP tools per conversation. If not, check whether you can select tools application-wide. Failing both of these options, curate your MCP servers by editing your client's configuration directly and disabling extra servers. Let's see how we can selectively activate tools in two popular clients. ### How to select tools in Cursor Click the gear icon at the top-right corner of the Cursor window, then select the **Tools & Integrations** tab. You'll see a list of all the installed MCP servers, with toggles for enabling servers individually. When you click the **X tools enabled** link below each server's name, you'll find a list of toggles that you can use to activate specific tools for that server. ### How to filter tools in Claude Desktop With a conversation open, click the "Search and tools" icon below the message box. Here, you'll see all the MCP servers you've installed. Each server has a submenu, where you can enable or disable tools individually or in bulk. Claude Desktop allows users to change tools mid-conversation, which leads us to think it may send a list of tools with each completion request to the model, thereby using even more tokens per conversation with many tools enabled. ## Tell the LLM which tools to use and when We've learned that too many tools can confuse LLMs, but occasionally, we simply need many tools in context. In these cases, help the LLM with tool selection. Rather than hoping the LLM will choose the right tools, be explicit in your instructions: - **Direct tool specification:** Instead of _"Help me analyze this data,"_ try _"Use the `read_csv` tool to load the data, then use `create_chart` to visualize the trends."_ - **Conditional tool usage:** Provide clear decision trees. For example, _"If the file is a CSV, use `read_csv`. If it's JSON, use `parse_json`. If neither works, use `read_file` and ask me for clarification."_ - **Tool sequencing:** When tasks require multiple steps, outline the expected tool sequence. For example, _"First use `web_search` to find recent articles, then `fetch_url` to get full content, then `summarize_text` to create a brief overview."_ - **Fallback strategies:** Define what to do when tools fail. For example, _"If `api_call` returns an error, use `log_error` to record it and try `alternative_api` instead."_ ## Include tool use pointers in your system prompt Include tips on using tools that you know you will almost always have enabled. For example, if you always have [Context7](https://context7.com/) and [MCP Run Python](https://github.com/pydantic/mcp-run-python) installed and active, include a note on how and when you expect the LLM to use the tools provided by these servers. Anecdotally, we've seen better results by including these tools and pointers in our system prompts: > When you need to do any calculations, always use the `run_python_code` tool. > Before using a library, make sure to get updated documentation from Context7 by calling `resolve-library-id`, followed by `get-library-docs`. ## This will only get easier In the long term, we hope we'll need to be less selective with tools for our LLMs. We're already seeing meta servers pop up that act as "tool selectors" or proxies, claiming to improve tool calling performance. We haven't seen any that can completely replace our roles as "human tool curators" in our workflows yet, but we think it is only a matter of time until a solid solution comes along. For now, keep pruning the list of tools at your LLM's disposal wherever possible, and provide tips on using tools when you think the LLM may get confused. # Arazzo: OpenAPI Workflows Source: https://speakeasy.com/openapi/arazzo import { Table } from "@/mdx/components"; The Arazzo Specification is a new addition to the OpenAPI Specification that allows you to define sequences of operations for your API. An Arazzo description is a separate document that references your OpenAPI document and describes how to combine operations from your OpenAPI document into step-by-step sequences. Arazzo descriptions are useful for: - Defining complex sequences of operations that involve multiple API calls. - Documenting the expected behavior of your API in a more structured way than a narrative description. - Generating code to execute the sequences of operations. - Generating tests to verify that the sequences of operations behave as expected. - Generating documentation that explains how to use the sequences of operations. ## Arazzo Description Structure An Arazzo description is a JSON or YAML document that follows the structure defined by the Arazzo Specification. ### Arazzo version | Field Name | Type | Required | | ---------- | -------- | -------- | | `arazzo` | `string` | ✅ | The version of the Arazzo Specification that the document uses. The value must be a supported [version number](#arazzo-specification-versions). ```yaml workflowsSpec: 1.0.0-prerelease ``` ### Info metadata | Field Name | Type | Required | | ---------- | --------------------------- | -------- | | `info` | [Info Object](#info-object) | ✅ | Provides metadata about the Arazzo description. ```yaml info: title: Speakeasy Bar Workflows summary: Workflows for managing the Speakeasy Bar API description: > This document defines workflows for managing the [Speakeasy Bar API](https://bar.example.com), including creating new drinks, managing inventory, and processing orders. version: 4.6.3 ``` ### Source description | Field Name | Type | Required | | -------------------- | --------------------------------------------------------- | -------- | | `sourceDescriptions` | [[Source Description Object](#source-description-object)] | ✅ | An array of [source description objects](#source-description-object) defining the OpenAPI or other documents containing the operations referenced by the workflows. The array must contain at least one source. ```yaml sourceDescriptions: - name: speakeasyBar url: https://bar.example.com/openapi.yaml type: openapi - name: printsAndBeeps url: https://output.example.com/workflows.yaml type: workflowsSpec ``` ### Workflows | Field Name | Type | Required | | ----------- | ------------------------------------- | -------- | | `workflows` | [[Workflow Object](#workflow-object)] | ✅ | An array of [workflow objects](#workflow-object) defining the workflows. The array must contain at least one workflow. ```yaml workflows: - workflowId: createDrink summary: Create a new drink in the bar's menu inputs: allOf: - $ref: "#/components/inputs/authenticate" - type: object properties: drink_name: type: string drink_type: type: string drink_price_usd_cent: type: integer ingredients: type: array items: type: string steps: - stepId: authenticate operationId: authenticate parameters: - reference: $components.parameters.username - reference: $components.parameters.password - stepId: createDrink operationId: createDrink parameters: - reference: $components.parameters.authorization - name: name in: query value: $inputs.drink_name - name: type in: query value: $inputs.drink_type - name: price in: query value: $inputs.drink_price_usd_cent - name: ingredients in: query value: $inputs.ingredients - workflowId: makeDrink summary: Order a drink and check the order status inputs: - name: orderType description: The type of order type: string required: true - name: productCode description: The product code of the drink type: string required: true - name: quantity description: The quantity of the drink type: integer required: true steps: - stepId: orderDrink operationId: createOrder parameters: - name: orderType in: body value: $inputs.orderType - name: productCode in: body value: $inputs.productCode - name: quantity in: body value: $inputs.quantity - stepId: checkStatus operationId: getOrder parameters: - name: orderNumber in: path value: $orderDrink.orderNumber successCriteria: - type: simple condition: $checkStatus.status == 'completed' onSuccess: - name: printReceipt type: goto workflowId: $sourceDescriptions.printsAndBeeps.printReceipt criteria: - type: simple condition: $checkStatus.status == 'completed' onFailure: - name: beepLoudly type: goto workflowId: $sourceDescriptions.printsAndBeeps.beepLoudly criteria: - type: simple condition: $checkStatus.status == 'failed' - workflowId: addIngredient summary: Add a new ingredient to the bar's inventory inputs: - name: username description: The username of the manager type: string required: true - name: password description: The password of the manager type: string required: true - name: ingredient_name description: The name of the ingredient type: string required: true - name: ingredient_type description: The type of the ingredient type: string required: true - name: ingredient_stock description: The stock of the ingredient type: integer required: true - name: productCode description: The product code of the ingredient type: string required: true steps: - stepId: authenticate operationId: authenticate parameters: - reference: $components.parameters.username value: admin - reference: $components.parameters.password - stepId: addIngredient operationId: createIngredient parameters: - reference: $components.parameters.authorization - name: name in: query value: $inputs.ingredient_name - name: type in: query value: $inputs.ingredient_type - name: stock in: query value: $inputs.ingredient_stock - name: productCode in: query value: $inputs.productCode components: inputs: authenticate: type: object properties: username: type: string password: type: string parameters: authorization: name: Authorization in: header value: $authenticate.outputs.token username: name: username in: body value: $inputs.username password: name: password in: body value: $inputs.password ``` This table shows all fields at the root of the Arazzo Specification:
## Arazzo Specification Versions The `arazzo` field contains the version number of the Arazzo Specification that the document conforms to. Tooling should use this value to interpret the document correctly. The current version of the Arazzo Specification is 1.0.0-prerelease, but keep in mind that the specification is still under development. ## Info Object Provides metadata about the Arazzo description.
Below is an example of an `info` object in an Arazzo document: ```yaml filename="arazzo.yaml" info: title: Speakeasy Bar Workflows summary: Workflows for managing the Speakeasy Bar API description: > This document defines workflows for managing the [Speakeasy Bar API](https://bar.example.com), including creating new drinks, managing inventory, and processing orders. version: 4.6.3 ``` ## Source Description Object A source description points to an OpenAPI document containing the operations referenced by the workflows in this document. It may also reference other Arazzo documents.
Below is an example of two source description objects in an Arazzo description document: ```yaml filename="arazzo.yaml" sourceDescriptions: - name: speakeasyBar url: https://bar.example.com/openapi.yaml type: openapi - name: printsAndBeeps url: https://output.example.com/workflows.yaml type: workflowsSpec ``` ## Workflow Object A workflow object defines a sequence of operations.
Below is an example of a workflow object: ```yaml filename="arazzo.yaml" workflows: - workflowId: createDrink summary: Create a new drink in the bar's menu inputs: allOf: - $ref: "#/components/inputs/authenticate" - type: object properties: drink_name: type: string drink_type: type: string drink_price_usd_cent: type: integer ingredients: type: array items: type: string steps: - stepId: authenticate operationId: authenticate parameters: - reference: $components.parameters.username - reference: $components.parameters.password - stepId: createDrink operationId: createDrink parameters: - reference: $components.parameters.authorization - name: name in: query value: $inputs.drink_name - name: type in: query value: $inputs.drink_type - name: price in: query value: $inputs.drink_price_usd_cent - name: ingredients in: query value: $inputs.ingredients - workflowId: makeDrink summary: Order a drink and check the order status inputs: - name: orderType description: The type of order type: string required: true - name: productCode description: The product code of the drink type: string required: true - name: quantity description: The quantity of the drink type: integer required: true steps: - stepId: orderDrink operationId: createOrder parameters: - name: orderType in: body value: $inputs.orderType - name: productCode in: body value: $inputs.productCode - name: quantity in: body value: $inputs.quantity - stepId: checkStatus operationId: getOrder parameters: - name: orderNumber in: path value: $orderDrink.orderNumber successCriteria: - type: simple condition: $checkStatus.status == 'completed' onSuccess: - name: printReceipt type: goto workflowId: $sourceDescriptions.printsAndBeeps.printReceipt criteria: - type: simple condition: $checkStatus.status == 'completed' onFailure: - name: beepLoudly type: goto workflowId: $sourceDescriptions.printsAndBeeps.beepLoudly criteria: - type: simple condition: $checkStatus.status == 'failed' - workflowId: addIngredient summary: Add a new ingredient to the bar's inventory inputs: - name: username description: The username of the manager type: string required: true - name: password description: The password of the manager type: string required: true - name: ingredient_name description: The name of the ingredient type: string required: true - name: ingredient_type description: The type of the ingredient type: string required: true - name: ingredient_stock description: The stock of the ingredient type: integer required: true - name: productCode description: The product code of the ingredient type: string required: true steps: - stepId: authenticate operationId: authenticate parameters: - reference: $components.parameters.username value: admin - reference: $components.parameters.password - stepId: addIngredient operationId: createIngredient parameters: - reference: $components.parameters.authorization - name: name in: query value: $inputs.ingredient_name - name: type in: query value: $inputs.ingredient_type - name: stock in: query value: $inputs.ingredient_stock - name: productCode in: query value: $inputs.productCode ``` ## Step Object A step object defines a single operation to perform as part of a workflow.
Below is an example of step objects in an Arazzo description document: ```yaml filename="arazzo.yaml" steps: - stepId: orderDrink operationId: createOrder parameters: - name: orderType in: body value: $inputs.orderType - name: productCode in: body value: $inputs.productCode - name: quantity in: body value: $inputs.quantity ``` ## Parameter Object A parameter object defines a single parameter to pass to an operation in a workflow step.
### Parameter Location For parameters passed to an operation, the `in` field specifies the location of the parameter. The possible values are: - `path` - The parameter is part of the operation's URL path. - `query` - The parameter is appended to the operation's URL as a query parameter. - `header` - The parameter is sent in the request headers. - `cookie` - The parameter is sent in a cookie. For parameters passed to a workflow, the `in` field must be omitted. Workflow parameters are always passed in the workflow's `inputs` object. ### Arazzo Parameter Object Examples ```yaml filename="arazzo.yaml" parameters: - reference: $components.parameters.authorization - name: name in: query value: $inputs.drink_name - name: type in: query value: $inputs.drink_type - name: price in: query value: $inputs.drink_price_usd_cent - name: ingredients in: query value: $inputs.ingredients ``` ## Success Action Object A success action object defines an action to take when a workflow step succeeds.
Below is an example of a success action object: ```yaml filename="arazzo.yaml" onSuccess: - name: printReceipt type: goto workflowId: $sourceDescriptions.printsAndBeeps.printReceipt criteria: - type: simple condition: $checkStatus.status == 'completed' ``` ## Failure Action Object A failure action object defines an action to take when a workflow step fails.
Below is an example of a failure action object: ```yaml filename="arazzo.yaml" onFailure: - name: beepLoudly type: goto workflowId: $sourceDescriptions.printsAndBeeps.beepLoudly criteria: - type: simple condition: $checkStatus.status == 'failed' ``` ## Components Object The `components` object holds reusable objects that can be referenced from other parts of the Arazzo description.
The keys in the `components` object may only contain alphanumeric characters, underscores, and dashes. ### Arazzo Components Object Example ```yaml filename="arazzo.yaml" components: inputs: authenticate: type: object properties: username: type: string password: type: string parameters: authorization: name: Authorization in: header value: $authenticate.outputs.token username: name: username in: body value: $inputs.username password: name: password in: body value: $inputs.password ``` The components defined in an Arazzo description are scoped to that document. Components defined in one Arazzo description cannot be referenced from another Arazzo description. ## Reusable Object A reusable object allows you to reference objects like success actions and failure actions defined in the `components` object from locations within steps or workflows.
### Arazzo Reusable Object Example ```yaml filename="arazzo.yaml" - reference: $components.parameters.username value: admin - reference: $components.parameters.password ``` ## Criterion Object A criterion object is used to specify assertions in the `successCriteria` field of a [step object](#step-object), or the `criteria` field of a [success action object](#success-action-object) or [failure action object](#failure-action-object). Criterion objects support three types of assertions: - `simple` - Basic comparisons using literals, operators, and [runtime expressions](#runtime-expressions). This is the default if no `type` is specified. - `regex` - Applies a regular expression pattern to a context defined by a [runtime expression](#runtime-expressions). - `JSONPath` - Applies a [JSONPath](https://goessner.net/articles/JsonPath/) expression to a context defined by a [runtime expression](#runtime-expressions). The root node of the JSONPath is the context. ### Literals Literals are constant values that can be used in `simple` conditions. The following data types are supported:
### Operators Simple conditions support the following operators:
`", description: "Greater than" }, { operator: "`>=`", description: "Greater than or equal" }, { operator: "`==`", description: "Equal" }, { operator: "`!=`", description: "Not equal" }, { operator: "`!`", description: "Not" }, { operator: "`&&`", description: "And" }, { operator: "`||`", description: "Or" }, { operator: "`()`", description: "Grouping" }, { operator: "`[]`", description: "Array index (0-based)" }, { operator: "`.`", description: "Property dereference" } ]} columns={[ { key: "operator", header: "Operator" }, { key: "description", header: "Description" } ]} /> String comparisons are case-insensitive. A criterion object consists of the following fields:
### Arazzo Criterion Object Examples **Simple Condition - Check if a drink was successfully created** ```yaml filename="arazzo.yaml" - condition: $statusCode == 201 ``` **Simple Condition - Check if the bar is open** ```yaml - condition: $response.body#/isOpen == true ``` **Regex Condition - Check if the drink name matches a pattern** ```yaml filename="arazzo.yaml" - context: $response.body#/drinkName condition: "^Sazerac" type: regex ``` **JSONPath Condition - Check if the ingredient list contains "whiskey"** ```yaml filename="arazzo.yaml" - context: $response.body condition: $..ingredients[?(@.name == 'whiskey')] type: JSONPath ``` **Simple Condition - Check if the customer is over 21** ```yaml filename="arazzo.yaml" - condition: $response.body#/customer/age >= 21 ``` **JSONPath Condition - Check if there are any available tables** ```yaml filename="arazzo.yaml" - context: $response.body condition: $[?length(@.tables[?(@.available == true)]) > 0] type: JSONPath ``` ## Request Body Object The request body object describes the payload and `Content-Type` to send with the request when executing an operation in a workflow step.
### Request Body Object Examples **JSON Payload (Template)** ```yaml filename="arazzo.yaml" contentType: application/json payload: | { "order": { "drinkId": "{$inputs.drink_id}", "customerId": "{$inputs.customer_id}", "quantity": {$inputs.quantity} } } ``` **JSON Payload (Object)** ```yaml filename="arazzo.yaml" contentType: application/json payload: order: drinkId: $inputs.drink_id customerId: $inputs.customer_id quantity: $inputs.quantity ``` **JSON Payload (Runtime Expression)** ```yaml filename="arazzo.yaml" contentType: application/json payload: $inputs.orderPayload ``` **XML Payload (Template)** ```yaml filename="arazzo.yaml" contentType: application/xml payload: | {$inputs.drink_id} {$inputs.customer_id} {$inputs.quantity} ``` **Form Data Payload (Object)** ```yaml filename="arazzo.yaml" contentType: application/x-www-form-urlencoded payload: drinkId: $inputs.drink_id customerId: $inputs.customer_id quantity: $inputs.quantity ``` **Form Data Payload (String)** ```yaml filename="arazzo.yaml" contentType: application/x-www-form-urlencoded payload: "drinkId={$inputs.drink_id}&customerId={$inputs.customer_id}&quantity={$inputs.quantity}" ``` ## Payload Replacement Object A payload replacement object specifies a location within the request payload and the value to insert at that location.
### Payload Replacement Object Examples **Runtime Expression Value** ```yaml target: /drinkId value: $inputs.drink_id ``` **Literal Value** ```yaml target: /quantity value: 2 ``` ## Runtime Expressions Runtime expressions allow you to reference values that will be available when a workflow is executed, such as values from the HTTP request or response, the event that triggered the workflow, or outputs from previous workflow steps. The syntax for runtime expressions is `{expression}`, where `expression` is one of the following:
### Runtime Expression Examples
## Arazzo Specification Extensions The Arazzo Specification allows custom properties to be added at certain points using specification extensions. Extension properties are always prefixed by `"x-"` and can have any valid JSON value. For example: ```yaml x-internal-id: abc123 x-vendor-parameter: vendorId: 123 channelId: abc ``` The extensions defined by the Arazzo Specification are:
The specification extension key formats `^x-oai-` and `^x-oas-` are reserved for extensions defined by the [OpenAPI Initiative](https://www.openapis.org/). Extension properties can be used to add additional features, metadata, or configuration options to the Arazzo description that are not directly supported by the current version of the specification. However, additional tooling may be required to process custom extensions. # Components in OpenAPI Source: https://speakeasy.com/openapi/components import { Table } from "@/mdx/components"; Components in OpenAPI are reusable bits of OpenAPI description, which can then be [referenced](/openapi/references). Reusing components allows for smaller file-sizes, reduces conflicts, and improves consistency across the API. Components can even be shared between multiple documents, allowing for improved reuse between multiple APIs. ```yaml components: schemas: User: type: object properties: id: type: integer name: type: string email: type: string format: email parameters: userId: name: uuid in: path description: Unique UUIDv4 of the user required: true schema: type: string format: uuid responses: NotFound: description: User not found content: application/json: schema: $ref: "#/components/schemas/Error" requestBodies: User: content: application/json: schema: $ref: "#/components/schemas/User" securitySchemes: bearerAuth: type: http scheme: bearer bearerFormat: JWT ``` Components can be referenced in other parts of the OpenAPI document using the `$ref` keyword. The `$ref` keyword is a JSON Pointer to the component, which is a string that starts with `#/components/` and then the component type and name. For example, to reference the `User` schema defined in the Components Object, you would use the following `$ref`: ```yaml responses: "200": description: User found content: application/json: schema: $ref: "#/components/schemas/User" ``` To put it all together, here is an example of how to reference all the various components in that previous example: ```yaml paths: /users/{userId}: get: summary: Get a user by UUID parameters: - $ref: "#/components/parameters/userId" responses: "200": description: User found content: application/json: schema: $ref: "#/components/schemas/User" "404": $ref: "#/components/responses/NotFound" security: - bearerAuth: [] ``` ## Components Object The Components Object is a map of reusable components broken down by type. ```yaml components: : : ``` The component name can be any valid string, but it is recommended to use a consistent naming convention across the API. A common naming convention is `PascalCase` or `camelCase`. ```yaml components: schemas: Train: Station: BookingPayments: ``` Here are the supported component types as of OpenAPI v3.1:
# Content and Media Types in OpenAPI Source: https://speakeasy.com/openapi/content import { Callout, Table } from "@/mdx/components"; In OpenAPI 3.1, the `content` keyword indicates the media types required in request bodies or returned by responses. Media types are often referred to as content types or MIME types, but we'll use media types in this document. Media types in OpenAPI inform the client how to interpret data received from the server, and which data types the server expects from the client. Common examples of media types include: - `application/json` for JSON objects. - `text/plain` for plain text. - `image/png` for PNG image files. - `application/xml` for XML files. - `multipart/form-data` for form data that can include files. ## Content Map in OpenAPI The `content` object is a map of key-value pairs. Each key in the map is a [media or MIME type](https://developer.mozilla.org/en-US/docs/Web/HTTP/Basics_of_HTTP/MIME_types) like `application/json`, `text/plain`, or `image/png`. The value associated with each key is a [Media Type Object](#media-type-object) that describes the structure and other relevant details for its corresponding media type. Media type keys can include wildcards indicating a range of media types they cover. For example, `application/*` would match `application/json`, `application/xml`, and so on. It can be explicitly defined to match only a single media type, for example, `application/json; charset=utf-8`. **Avoid wildcard media types where possible:** While using wildcards in defining content types is convenient, it might lead to ambiguous results if the client and server do not handle the same range of media types. Use specific media types where possible to avoid ambiguity. Where both a wildcard and a specific media type are defined, the specific media type definition takes precedence. The example below shows a `content` map with four media types: ```yaml content: application/json: # JSON formatted content schema: $ref: "#/components/schemas/Drink" img/*: # Image formatted content of any type schema: type: string format: binary text/*: # Text-based content of any type schema: type: string text/csv: # CSV formatted content (this will take precedence over text/*) schema: $ref: "#/components/schemas/Drink" ``` In this example, the server expects one of the following types: - A JSON object representing a drink. - Any image file in binary format. - A CSV file representing a drink. - Any text file. ## Content Negotiation When the client sends a request to the server, it includes a `Content-Type` HTTP header in the request, indicating to the server how to interpret the data in the body of the request. Likewise, the server includes a `Content-Type` HTTP header in its response, which the client should use to interpret the data in the response. The client may also include an `Accept` HTTP header in a request, indicating to the server which content types the client can handle. The server should then send a response with a `Content-Type` header that matches one of the accepted types. This exchange is known as [content negotiation](https://developer.mozilla.org/en-US/docs/Web/HTTP/Content_negotiation). The diagram below illustrates the headers sent by the client and server during content negotiation: ```mermaid sequenceDiagram participant C as Client participant S as Server Note over C,S: Establish Connection C->>S: Request with Headers Note over C: Request headers include: Note over C: Content-Type: text/csv Note over C: Accept: application/json, application/xml S->>C: Response with Headers Note over S: Response headers include: Note over S: Content-Type: application/json ``` Note that the request and response content types do not need to match. For example, in the diagram above, the client sends a request as CSV but expects JSON or XML in response. ## Media Type Object A Media Type Object describes the request or response for a media type, with optional examples and extensions.
## Media Type Examples The examples below illustrate the use of the `content` object with different media types. ### JSON Media Type The example below shows a `content` object with a JSON media type: ```yaml content: application/json: schema: $ref: "#/components/schemas/Drink" examples: mojito: value: name: "Mojito" ingredients: - name: "White Rum" quantity: 50 - name: "Lime Juice" quantity: 20 - name: "Mint Leaves" quantity: 10 ``` In this example, the server expects a JSON object representing a drink. The `examples` field provides an [Example Object](/openapi/examples) of the expected JSON object. The curl command below sends a request to the server with a JSON object in the body: ```bash curl -X POST "https://api.example.com/drinks" \ -H "Content-Type: application/json" \ -d '{ "name": "Mojito", "ingredients": [ { "name": "White Rum", "quantity": 50 }, { "name": "Lime Juice", "quantity": 20 }, { "name": "Mint Leaves", "quantity": 10 } ] }' ``` ### Image Media Type The example below shows a `content` object with an image media type: ```yaml content: image/png: schema: type: string format: binary ``` In this example, the server expects an image file in binary format. The curl command below sends a request to the server with an image file in the body: ```bash curl -X POST "https://api.example.com/images" \ -H "Content-Type: image/png" \ --data-binary @image.png ``` ### Text Media Type The example below shows a `content` object with a text media type: ```yaml content: text/plain: schema: type: string ``` In this example, the server expects a plain text file. The curl command below sends a request to the server with a text file in the body: ```bash curl -X POST "https://api.example.com/text" \ -H "Content-Type: text/plain" \ -d "Hello, World!" ``` ### CSV Media Type The example below shows a `content` object with a CSV media type: ```yaml content: text/csv: schema: $ref: "#/components/schemas/Drink" ``` In this example, the server expects a CSV file representing a drink. The curl command below sends a request to the server with a CSV file in the body: ```bash curl -X POST "https://api.example.com/csv" \ -H "Content-Type: text/csv" \ -d "Mojito,White Rum,50,Lime Juice,20,Mint Leaves,10" ``` ### Multipart Form Data The example below shows a `content` object with a multipart form data media type: ```yaml content: multipart/form-data: schema: properties: photo: description: A photo of the drink. type: string format: binary recipe: description: The recipe for the drink. type: string name: description: The name of the drink. type: string encoding: photo: contentType: image/jpeg, image/png headers: Content-Disposition: description: Specifies the disposition of the file (attachment and file name). schema: type: string default: 'form-data; name="photo"; filename="default.jpg"' allowReserved: false recipe: contentType: text/plain headers: Content-Disposition: description: Specifies the disposition of the file (attachment and file name). schema: type: string default: 'form-data; name="recipe"; filename="default.txt"' allowReserved: false name: contentType: text/plain headers: Content-Disposition: description: Specifies the disposition of the field. schema: type: string default: 'form-data; name="name"' allowReserved: false ``` In this example, the server expects a form data request with a photo of the drink, the recipe for the drink, and the name of the drink. The `encoding` field provides additional information about each part, such as the content type, headers, and whether reserved characters are allowed. The curl command below sends a request to the server with a photo file, a recipe file, and the name of the drink in the body: ```bash curl -X POST "https://api.example.com/drinks" \ -F "photo=@photo.jpg;type=image/jpeg" \ -F "recipe=@recipe.txt;type=text/plain" \ -F "name=Mocktail" ``` ## OpenAPI Content Best Practices When designing APIs with OpenAPI, consider the following best practices for content and media types: - Where possible, use the most specific media type for your content. For example, prefer `application/json` over `application/*` if your content is JSON. - When using OpenAPI 3.1, provide at least one example for each media type using the `examples` keyword to help clients understand the expected content and enrich the API documentation. # File uploads in OpenAPI Source: https://speakeasy.com/openapi/content/file-uploads import { CodeWithTabs } from "@/mdx/components"; File uploads are a critical part of powerful REST APIs, allowing files to be transmitted from clients to servers for storage, analysis, or processing. File uploads in a REST API can be handled in various ways, depending on file format, size, and complexity. OpenAPI provides a standardized way to define and describe file uploads, accommodating diverse use cases. OpenAPI facilitates file uploads through various media types, including `multipart/form-data`, `application/octet-stream` (binary), and JSON-embedded payloads. Each approach has advantages and challenges that should be evaluated based on the API's requirements. ## Why file uploads can be tricky File uploads present several technical and operational challenges, such as: - **Content type complexity:** Servers need to process different content types differently. - **Validation and error handling:** Validating and handling file uploads can be complex, especially when dealing with large files or multiple file uploads. - **Security and privacy:** File uploads are susceptible to security and privacy risks, such as data exfiltration, resource abuse, file injection, and data breaches. - **Scalability and performance:** Handling large file uploads is resource-intensive and can impact server performance, so a good strategy for streaming and buffering is needed. These challenges can be mitigated with the right approach and tools, but also the correct specification, so the client and server can communicate effectively. These challenges can be mitigated by following best practices for efficient file handling and ensuring the specification allows the client and server to communicate effectively. ## Defining file uploads in an OpenAPI document File uploads can be described in an OpenAPI document using `multipart/form-data`, `application/octet-stream` (binary), or JSON. ### `multipart/form-data` Widely recognized as the standard for file uploads, `multipart/form-data` allows files and associated metadata to be transmitted in the same request. In an OpenAPI document, specifying `multipart/form-data` for the `/file/upload` endpoint might look like this: ```yaml filename="openapi.yaml" paths: /file/upload: post: summary: Upload a file description: > Allows uploading a file along with additional metadata. operationId: uploadFile tags: - FileUpload requestBody: required: true content: multipart/form-data: schema: type: object properties: file: type: string format: binary caption: type: string responses: '200': description: File uploaded successfully ``` The client can call the endpoint to upload a PDF file with a metadata description like this: ### `application/octet-stream` (binary) The `application/octet-stream` media type is used to transmit binary data, such as images, audio, or video files. It is commonly used for file uploads, but it can also be used for other types of binary data. In an OpenAPI document, specifying `application/octet-stream` for the `/file/upload` endpoint might look like this: ```yaml filename="openapi.yaml" paths: /file/upload: post: summary: Upload a file requestBody: required: true content: application/octet-stream: schema: type: string format: binary responses: "200": description: File uploaded successfully ``` The client can call the endpoint to upload an image file as follows: In curl, the `--data-binary` option sends raw file data in the HTTP request body, specified with `@` followed by the file path. Similarly, in Python, a file opened in binary mode (`rb`) is read and sent as a binary string. ### JSON For specific use cases, the JSON format can be used to transmit a file to a server, such as when the file is small and requires little or no processing. The JSON format is not suitable for large files or files with complex structures, as it can lead to issues such as data loss or corruption. In an OpenAPI document, the specification for transmitting a JSON file on the `/file/upload` endpoint might look like this: ```yaml filename="openapi.yaml" paths: /file/upload: post: summary: Upload a file requestBody: required: true content: application/json: schema: type: object properties: file: type: string format: byte caption: type: string responses: "200": description: File uploaded successfully ``` The client can call the endpoint using curl or Python as follows: In this example, the file data is encoded in Base64 format and included in the JSON payload. The Python code reads the file data from a string variable and sends it as a JSON object in the request body. ## Best practices for file uploads Here are some best practices for handling file uploads in an OpenAPI document. ### Prioritize `multipart/form-data` The `multipart/form-data` format is preferred for most file uploads, as it allows additional metadata to be sent with the file. However, other formats like binary or JSON can be used for specific use cases, such as streaming large files (binary) or embedding files within JSON payloads for system integration. ### Handling large files Uploading large files can significantly impact the performance of your API. To avoid server overload, consider using a streaming approach instead of sending or processing the whole file at once on the server. The `application/octet-stream` media type is ideal for this use case, as it allows the file to be divided into smaller binary chunks to be sent one by one. Chunked file uploads require some extra coding work as there is no formal specification for upload streams. However, this process can be implemented using a **session-based streaming** approach, which involves the following steps: 1. **Initiate the session:** The client requests an upload session, and the server responds with a session ID to track the upload. 2. **Upload chunks:** The client slices the file into smaller binary chunks and uploads them sequentially, using the session ID to associate them. 3. **Finalize the upload:** The client signals the server to finalize the upload, and the server assembles the chunks into the complete file. In an OpenAPI document, the specification for this approach might look like this: ```yaml filename="openapi.yaml" openapi: 3.1.0 info: title: File Upload API version: 1.0.0 paths: /init: post: summary: Initiate an upload session operationId: initiateSession responses: 200: description: Upload session initiated content: application/json: schema: type: object properties: session_id: type: string description: The unique ID for the upload session 400: description: Bad request /upload: post: summary: Upload a file chunk operationId: uploadChunk parameters: - name: x-session-id in: header required: true schema: type: string description: The unique session ID for associating the chunk requestBody: required: true content: application/octet-stream: schema: type: string format: binary responses: 200: description: Chunk uploaded successfully 400: description: Invalid or missing session ID /finalize: post: summary: Finalize the upload session operationId: finalizeUpload parameters: - name: x-session-id in: header required: true schema: type: string description: The unique session ID for finalizing the upload responses: 200: description: Upload finalized successfully 400: description: Invalid or missing session ID ``` The client begins by sending a POST `/init` request to the server to initiate an upload session, receiving a `session_id` in the response. The file is then divided into smaller binary chunks, and each chunk is sent using a POST `/upload` request with the `X-Session-ID` header set to the `session_id`. The chunk is included in the request body as `application/octet-stream`. When all chunks are uploaded, the client sends a POST `/finalize` request including the `X-Session-ID` header to instruct the server to validate and assemble the chunks into the complete file, ensuring a reliable and efficient upload process. ### Provide clear and informative feedback If an error occurs during the upload process, the client should receive clear and informative feedback from the server. For example, if the server data within the request body is invalid, the client should receive a `400 Bad Request` response with a clear error message. If the file is too large, the client should receive a `413 Request Entity Too Large` response. Here's how you can define these responses in your OpenAPI document: ```yaml filename="openapi.yaml" responses: 400: description: Bad Request - Validation error content: application/json: schema: type: object properties: error: type: string example: "Invalid file format. Only JPEG and PNG are allowed." 413: description: Payload Too Large - File size exceeds limit content: application/json: schema: type: object properties: error: type: string example: "File size exceeds the maximum allowed limit of 10MB." ``` # JSONL responses in OpenAPI Source: https://speakeasy.com/openapi/content/jsonl JSON Lines (JSONL) is a convenient format for storing structured data that may be processed one record at a time. It's a simple format where each line is a valid JSON value, typically a JSON object or array. JSONL is particularly useful for handling large datasets, streaming data, or log files where each line represents a separate record. ## Understanding JSONL format JSONL (also known as newline-delimited JSON) consists of multiple JSON objects, with each object on a separate line. Each line must be a valid JSON value, and lines are separated by a newline character (`\n`). More details on the format can be found on [JsonLines Docs](https://jsonlines.org/) Here's an example of a JSONL file: ``` {"name": "Alice", "age": 30, "city": "New York"} {"name": "Bob", "age": 25, "city": "San Francisco"} {"name": "Charlie", "age": 35, "city": "Chicago"} ``` JSONL offers several advantages over traditional JSON: - **Streaming**: JSONL can be processed one line at a time, making it ideal for streaming applications. - **Append-friendly**: New records can be easily appended to the end of a JSONL file. - **Memory-efficient**: Processing JSONL doesn't require loading the entire dataset into memory. - **Parallelization**: JSONL data can be easily split and processed in parallel. ## Defining JSONL responses in OpenAPI documents JSONL responses can be defined in OpenAPI by using the `application/jsonl` or `text/jsonl` MIME type. While JSONL isn't natively supported in the OpenAPI specification, you can use these content types to indicate that the response will be in JSONL format. Here's an example of how to define a JSONL response in an OpenAPI document: ```yaml filename="openapi.yaml" paths: /users/export: get: tags: - Users summary: Export user data in JSONL format description: > This endpoint returns user data in JSONL format, with each line containing a complete user record. This format is ideal for large datasets that need to be processed one record at a time. responses: '200': description: User data in JSONL format content: application/jsonl: schema: $ref: '#/components/schemas/User' '400': description: Invalid request '500': description: Internal server error components: schemas: User: type: object required: [id, name, email] properties: id: type: string format: uuid description: Unique identifier for the user name: type: string description: User's full name email: type: string format: email description: User's email address age: type: integer description: User's age city: type: string description: User's city of residence ``` In this example, the `/users/export` endpoint returns user data in JSONL format. Each line of the response will be a valid JSON object representing a user, as defined by the `User` schema. ## Client-side handling of JSONL responses When working with JSONL responses, clients need to process the data line by line. Here's an example of how to handle JSONL responses using a python SDK generated by Speakeasy: ```python from openapi import SDK with SDK() as sdk: res = sdk.users.get_users_export() with res as jsonl_stream: for event in jsonl_stream: # handle event print(f"User: {event['name']}, Email: {event['email']}") ``` In this example, the SDK handles the streaming of JSONL data, allowing you to process each record as it arrives. The context manager (`with res as jsonl_stream`) ensures proper resource cleanup after processing. ## Best practices for JSONL API design When designing APIs that return JSONL responses, consider the following best practices: ### Use appropriate content types Use the `application/jsonl` or `text/jsonl` content type to clearly indicate that the response is in JSONL format. This helps clients understand how to process the response correctly. ### Include clear documentation Provide clear documentation about the JSONL format and how clients should process it. Include examples of the response format and client-side code for handling JSONL data. ### Consider pagination for large datasets Even though JSONL is efficient for streaming large datasets, consider implementing pagination to allow clients to request smaller chunks of data. This can be done using query parameters like `limit` and `offset`. ```yaml filename="openapi.yaml" paths: /users/export: get: parameters: - name: limit in: query description: Maximum number of users to return schema: type: integer default: 100 - name: offset in: query description: Number of users to skip schema: type: integer default: 0 ``` # Server-sent events in OpenAPI Source: https://speakeasy.com/openapi/content/server-sent-events import { Table } from "@/mdx/components"; Server-sent events (SSE) allow servers to push real-time updates to clients over a single HTTP connection. This protocol is widely used for scenarios requiring steady updates, such as notifications, live data feeds, or chat applications. While SSE shares some conceptual similarities with the WebSocket protocol, SSE differs significantly as it is a one-way communication from the server to the client, whereas the WebSocket protocol supports full-duplex communication. The following table summarizes the main differences between SSE and WebSockets.
SSE is ideal for simple, efficient, one-way server-to-client communication scenarios. The WebSocket protocol is better suited to applications that require interactive, low-latency, two-way communication between the client and server. ## Defining SSE in OpenAPI documents Server-sent events (SSE) are not natively supported in OpenAPI but can be represented as an endpoint using the `text/event-stream` MIME type to indicate the data format. The event stream format is a UTF-8-encoded text stream with messages separated by a newline (`\n`). Each message may include up to four fields: - `event`: A string specifying the event type. - `data`: The payload, often a JSON object or plain text. - `id`: An optional unique identifier for resuming streams after disconnection. - `retry`: An optional integer defining reconnection delay in milliseconds. Depending on application needs, messages can include only the `data` field, only the `event` field, or a combination of fields. This flexibility allows for tailored implementations, for example, a data-only stream for updates or an event-only stream for simple notifications. This example SSE endpoint notifies the client about stock price updates and includes only the `id`, `event`, and `data` fields: ```yaml filename="openapi.yaml" paths: /stock-updates: get: tags: - ServerSentEvents summary: Subscribe to real-time stock market updates description: > This endpoint streams real-time stock updates to the client using server-sent events (SSE). The client must establish a persistent HTTP connection to receive updates. responses: "200": description: Stream of real-time stock updates content: text/event-stream: schema: $ref: "#/components/schemas/StockStream" "400": description: Invalid request "500": description: Internal server error components: schemas: StockStream: type: object description: A server-sent event containing stock market update content required: [id, event, data] properties: id: type: string description: Unique identifier for the stock update event event: type: string const: stock_update description: Event type data: $ref: "#/components/schemas/StockUpdate" StockUpdate: type: object properties: symbol: type: string description: Stock ticker symbol price: type: string description: Current stock price example: "100.25" ``` A JavaScript client can subscribe to the endpoint using the `EventSource` API: ```javascript const eventSource = new EventSource("https:://api.example.com/stock-updates"); eventSource.onmessage = function (event) { // The event has the following format as example: // {"id":"1","event":"stock_update","data":{"symbol":"AAPL","price":"100.25"}} const stockUpdate = JSON.parse(event).data; console.log( `Stock Update: ${stockUpdate.symbol} is now ${stockUpdate.price}`, ); }; eventSource.onerror = function (error) { console.error("Error occurred:", error); }; ``` ## Best practices for SSE design and OpenAPI integration Here are some best practices for handling server-sent events and including them in an OpenAPI document. ### Improve reliability with heartbeats Sending a heartbeat every few seconds is recommended to improve reliability by keeping the connection alive. Heartbeats can also help detect network issues and prompt the client to reconnect. If you implement heartbeats, your SSE APIs can send multiple types of events, allowing you to use the `oneOf` keyword to describe the heartbeat message format. ```yaml filename="openapi.yaml" components: schemas: StockStream: oneOf: - $ref: "#/components/schemas/HeartbeatEvent" - $ref: "#/components/schemas/StockUpdateEvent" discriminator: propertyName: event mapping: ping: "#/components/schemas/HeartbeatEvent" stock_update: "#/components/schemas/StockUpdateEvent" HeartbeatEvent: description: A server-sent event indicating that the server is still processing the request type: object required: [event] properties: event: type: string const: "ping" timestamp: type: string format: date-time description: Timestamp of the heartbeat StockUpdateEvent: description: A server-sent event containing stock market update content type: object required: [id, event, data] properties: id: type: string description: Unique identifier for the stock update event event: type: string const: stock_update ``` ### Include event identification in the event payload Include an `id` or `sequence` property in the event payload to ensure that the client receives events in the correct order and avoid missing or out-of-order updates. ### Implement a retry mechanism To prevent data loss when an API fails to send an event, implement a retry mechanism such as introducing a delay before retrying or using exponential backoff. ### Use sentinel events to signal a closed connection Sending a sentinel event can be helpful to indicate that the connection is closed or the server is no longer available. This is useful for error handling or notifying the client that there is no more data to be received. The following example schema for a sentinel event demonstrates how a client can terminate a connection when it receives the `CLOSED` sentinel event: ```yaml filename="openapi.yaml" paths: /stock-updates: get: summary: Subscribe to real-time stock market updates operationId: stockUpdates tags: - ServerSentEvents responses: "200": description: Stream of real-time stock updates content: text/event-stream: x-speakeasy-sse-sentinel: "CLOSED" # Speakeasy extension for sentinel events schema: $ref: "#/components/schemas/StockUpdateEvent" ``` For each event received, the client can check the `X-SSE-Sentinel` header to determine whether the connection has closed or if no more data needs to be received. ### Handle SSE errors effectively To handle errors in SSE effectively, servers can send specific error events within the stream that include an error field detailing the issue. For non-critical errors, custom headers like `X-SSE-Error` can communicate problems without interrupting the event flow. Specific error events in the stream can be defined using a structured schema, as demonstrated below. ```yaml filename="openapi.yaml" components: schemas: ErrorEvent: description: A server-sent error event type: object required: [event, message] properties: event: type: string const: error message: type: string description: Description of the error ``` For critical errors, use a sentinel event as described in the previous section. # Examples in OpenAPI Source: https://speakeasy.com/openapi/examples import { Table } from "@/mdx/components"; ## Why add examples to your OpenAPI spec? Adding examples to your OpenAPI spec significantly improves the readability of generated artifacts, like documentation and SDKs, by providing concrete, real-world illustrations of how your API behaves. This reduces the learning curve and potential for user errors. ## Where to add examples to your OpenAPI spec You can add examples to **objects, parameters, or properties** using either the `example` or `examples` keyword. In the examples below, we consider an API endpoint (e.g., `/ingredients/{id}`) that returns the following example response. This response will be defined in the OpenAPI spec using an `Ingredient` object: ```json { "id": 123, "name": "Sugar Syrup", "type": "long-life", "stock": 10, "photo": "https://speakeasy.bar/ingredients/sugar_syrup.jpg", "status": "available" } ``` ## 1. Single Example (`example` Keyword) ### Defining a Single Example within `components.schemas` Use the `example` keyword to define a single example directly within the `schemas` section. This provides immediate context without requiring users to reference other parts of the spec. However, since it splits the example across multiple properties, it can make understanding the entire object more difficult and may lead to duplication (and maintenance issues) if the same example is needed in multiple schemas. ```yaml components: schemas: Ingredient: type: object properties: id: type: integer example: 123 name: type: string example: "Sugar Syrup" type: type: string example: "long-life" stock: type: integer example: 10 photo: type: string format: uri example: "https://speakeasy.bar/ingredients/sugar_syrup.jpg" status: type: string enum: - available - out-of-stock example: "available" ``` ### Defining a Single Example within `components.examples` Alternatively, you can define the example as a reusable Example Object in the `components.examples` section. This approach helps avoid redundancy and improves maintainability, as the example can be referenced across multiple schemas, requests, or responses. This is the format of the Example Object:
```yaml components: examples: SugarSyrup: summary: An example of a sugar syrup ingredient. value: id: 123 name: "Sugar Syrup" type: "long-life" stock: 10 photo: "https://speakeasy.bar/ingredients/sugar_syrup.jpg" status: "available" ``` **Referencing the Reusable Example in `components.schemas`** Once defined in `components.examples`, you can reference this reusable example in your schema like this: ```yaml components: schemas: Ingredient: type: object properties: id: type: integer name: type: string type: type: string stock: type: integer photo: type: string format: uri status: type: string enum: - available - out-of-stock examples: $ref: '#/components/examples/SugarSyrup' ``` ## 2. Multiple Examples (`examples` Keyword) To provide multiple examples for a property, or for the entire object, you can use the `examples` keyword. This is useful for showing different scenarios or variations, such as different statuses or conditions for an ingredient. #### Defining Multiple Examples within `components.schemas` As before, the `examples` can be specified within `components.schemas`. Here's how you can use the examples keyword at both the property level and the object level. **Using examples at the Property Level** ```yaml components: schemas: Ingredient: type: object properties: id: type: integer examples: - 123 - 125 name: type: string examples: - "Sugar Syrup" - "Maple Syrup" type: type: string examples: - "long-life" - "organic" stock: type: integer examples: - 10 - 0 photo: type: string format: uri examples: - "https://speakeasy.bar/ingredients/sugar_syrup.jpg" - "https://speakeasy.bar/ingredients/maple_syrup.jpg" status: type: string enum: - available - discontinued examples: - "available" - "discontinued" ``` The above uses a concise array underneath each `examples`. The drawback of this approach is that it doesn't allow for additional metadata such as `summary` or `description`. An alternative approach that does allow for such metadata is e.g.: ```yaml ... photo: type: string format: uri examples: sugarSyrup: summary: A photo of Sugar Syrup. value: "https://speakeasy.bar/ingredients/sugar_syrup.jpg" mapleSyrup: summary: A photo of Maple Syrup. value: "https://speakeasy.bar/ingredients/maple_syrup.jpg" status: type: string enum: - available - discontinued examples: available: summary: Status for available ingredient. value: "available" discontinued: summary: Status for discontinued ingredient. value: "discontinued" ``` **Using examples at the Object Level** An alternative approach to defining examples at the property level is to define them at the object level. This shows entire scenarios for the object (`Ingredient` in this case). Each example (`available` and `discontinued`) is a complete instance of the object with all its properties. ```yaml components: schemas: Ingredient: type: object properties: id: type: integer name: type: string type: type: string stock: type: integer photo: type: string format: uri status: type: string enum: - available - discontinued examples: available: value: id: 123 name: "Sugar Syrup" type: "long-life" stock: 10 photo: "https://speakeasy.bar/ingredients/sugar_syrup.jpg" status: "available" discontinued: value: id: 125 name: "Maple Syrup" type: "organic" stock: 0 photo: "https://speakeasy.bar/ingredients/maple_syrup.jpg" status: "discontinued" ``` Note that the specific names used here (`available` and `discontinued`) are not referenced elsewhere. In other words, you could use names like `example1` or `example2` without affecting the validity of the OpenAPI spec. However, descriptive names like `available` and `discontinued` are important for readability. ### Defining Multiple Examples as Reusable Objects in `components.examples` Multiple examples can also be defined as a reusable object in the `components.examples` section. This approach keeps examples grouped together, making them easier to maintain and reuse across different parts of the API specification: ```yaml components: examples: AvailableIngredient: summary: An example of an ingredient that is available. value: id: 123 name: "Sugar Syrup" type: "long-life" stock: 10 photo: "https://speakeasy.bar/ingredients/sugar_syrup.jpg" status: "available" DiscontinuedIngredient: summary: An example of an ingredient that has been discontinued. value: id: 125 name: "Maple Syrup" type: "organic" stock: 0 photo: "https://speakeasy.bar/ingredients/maple_syrup.jpg" status: "discontinued" ``` **Referencing Multiple Reusable Examples in `components.schemas`** Once defined, you can reference the examples in your schemas like this: ```yaml components: schemas: Ingredient: type: object properties: id: type: integer name: type: string type: type: string stock: type: integer photo: type: string format: uri status: type: string enum: - available - discontinued examples: available: $ref: '#/components/examples/AvailableIngredient' discontinued: $ref: '#/components/examples/DiscontinuedIngredient' ``` Here of course, the names `AvailableIngredient` and `DiscontinuedIngredient` are important since they are referenced elsewhere. # Extensions in OpenAPI Source: https://speakeasy.com/openapi/extensions import { Table } from "@/mdx/components"; Extensions allow us to add extra keywords not included in the OpenAPI Specification. This enables tooling such as SDK generators to access vendor-specific functionality directly in an OpenAPI document. Extension fields always start with `x-`. Although optional, it is conventional for vendors to further prefix their extensions with the name of the vendor. For example, Speakeasy uses extensions that start with `x-speakeasy-`. This makes it easier to track vendor extensions over time and remove unused vendor extensions in the future. The value of an extension field can be an object, array, `null`, or any primitive value. Vendors determine the values they expect for the extensions they use.
Here's an example of a Speakeasy extension that adds retries to requests made by Speakeasy-managed SDKs: ```yaml x-speakeasy-retries: strategy: backoff backoff: initialInterval: 500 # 500 milliseconds maxInterval: 60000 # 60 seconds maxElapsedTime: 3600000 # 5 minutes exponent: 1.5 statusCodes: - 5XX retryConnectionErrors: true ``` # The OpenAPI External Documentation Object Source: https://speakeasy.com/openapi/external-documentation import { Table } from "@/mdx/components"; Allows for providing information about external documentation available for the API, Operation, Tag, or Schema.
# Server Framework Guides Source: https://speakeasy.com/openapi/frameworks Here, you'll find a comprehensive collection of step-by-step guides for generating OpenAPI specs for popular server frameworks in various languages. Or read our guide below for advice on how to generate an OpenAPI schema from your existing code base. import { CardGrid } from "@/components/card-grid"; import { GoFrameworkGuidesData, GRPCFrameworkGuidesData, JavaFrameworkGuidesData, PHPFrameworkGuidesData, PythonFrameworkGuidesData, RubyFrameworkGuidesData, TypescriptFrameworkGuidesData, } from "@/lib/data/openapi/framework-guides"; # How to generate an OpenAPI schema from your code An [OpenAPI schema](https://www.openapis.org/) (also called an OpenAPI specification) is a text description of your web service in detail. Your customers and automated tools can use this document to interact with your service with the confidence that they know exactly what each operation does and how to use it. Some companies start designing their software service in an OpenAPI schema and use that schema to generate their server code. This is called the schema-first approach. But it's more likely you started building software to service a market, then realized the benefits of providing OpenAPI tools to your clients, and now want to know how to create and maintain an OpenAPI schema from your existing server code. This is the code-first approach. This guide will explain how to generate a schema from code, and link to all the necessary tools in the programming language you use. But first, let's summarize the benefits of using OpenAPI and the variations of the code-first approach that you can choose from. ## The benefits of OpenAPI If you don't have an OpenAPI schema, you still need to write documentation, with examples, explaining to your clients how they can call your API. You also need to update this documentation whenever the code changes on your server. Writing this documentation in the standard OpenAPI format provides the following benefits: - Enables your clients to browse the schema in their tool of choice, such as [Swagger UI](https://swagger.io/tools/swagger-ui), with good usability. - Allows the schema to be used for automatic SDK generation, enabling your clients to call your server directly from code without needing to handle REST web calls or use curl. Even if you don't generate an SDK, using an OpenAPI schema to validate the code that clients use to call your service can reduce confusion and errors. - Supports the generation of server code stubs with statically typed request parameters and responses that you can use to validate that your server code matches the schema. ## Different workflows to support OpenAPI If you take a code-first approach, you can either annotate your existing code with special comments that an automated tool uses to generate an OpenAPI schema, or start coding in a meta-framework that generates both the server code and the OpenAPI schema. (We call it a meta-framework because it sits above both your code and your schema, controlling both.) There are two types of annotation: - Text comments, which are not part of the programming language in which your server is written. - Attributes, which are valid code in the programming language. Attributes are superior, as the compiler and your IDE can check that your annotations are syntactically valid code as you work. Attributes can also be refactored, unlike comments. The danger of using annotations is human error. Programmers need to update annotations correctly whenever they change an operation, but they may forget to or make mistakes. Even when attribute annotations are syntactically correct, they may still describe the operation incorrectly. This will cause your code and schema to drift apart, potentially causing errors when clients try to call your API. Using a meta-framework, on the other hand, automatically synchronizes your code and schema but requires writing code in the new framework, adding a compilation step, and learning a new tool. A third option that falls between annotations and meta-frameworks is to use an OpenAPI-aware web framework. These frameworks are designed with native support for OpenAPI features and can automatically generate an OpenAPI schema from the code you write, usually with more detail than that provided in annotations. OpenAPI-aware frameworks, however, tend to be very new, and the most popular (and older) frameworks, like Django and Laravel, are not OpenAPI-aware. Note that the workflow options we discuss here depend on the availability of tools to support them in your programming language. Popular web languages will have tools for a range of options, but obscure languages may not. You will also find it easier to use a statically typed language, like Go, than a dynamic one, like PHP. The following process will probably work best for most companies wanting to support OpenAPI: - Generate an OpenAPI schema for your code, as we describe in the rest of this article. - Use the new schema as your principal source from now on and generate server code from the schema rather than the other way around. (There is a section explaining schema-first workflows at the end of this guide.) - Generate SDKs from the schema to make it simple for your clients to call your API. If you want to keep your code as the principal source, you may want to include an automated reminder in your version-control process to tell programmers to update annotations whenever they commit code. ## Four techniques to extract an OpenAPI schema The next four sections will show you different ways to generate an OpenAPI schema document from your existing code by: - Recording network traffic - Adding comment annotations - Adding attribute annotations and changing your code to use an OpenAPI-aware framework - Rewriting your code using a meta-framework Each technique varies depending on the programming language. This guide provides examples in Go, as it's a popular, statically typed language specifically designed for web services. The first thing to check is whether your web server framework already natively supports OpenAPI. If it does, you can stop reading this article and follow the framework documentation on how to access the schema the framework automatically creates. If not, read on to discover your options. If you don't have an API yet but want to learn about OpenAPI for future projects, choose a server framework and language with native OpenAPI support. Alternatively, start by documenting your proposed API as a schema using a Swagger tool, then use a framework that supports code generation from the schema. ## Use HAR and APIGit to generate a schema The first way to create a schema is to use a tool that listens to your network calls and extracts the schema automatically from the HTTP calls it sees. Generating a schema from network traffic does not depend on programming language. The process is the same for all languages. The benefit of this approach is that you don't have to manually read your code and document (potentially with mistakes) what the calling contract should be. Instead, the network tool will tell you what actual data is sent, allowing you to spot any unintended parameters when reading through the schema after creation. The disadvantage is that you have to manually call every operation in your API with all possible parameters to ensure the generator records all variations and creates an accurate schema. For smaller APIs, you might prefer manually adding annotations or writing code to generate a schema. But for any other project, we recommend using this network listener approach, even in conjunction with other approaches, as it provides an automated review of your existing API. ### HAR example Let's use the standard Pet Store OpenAPI example to demonstrate how to use a network listener. - Browse to https://petstore31.swagger.io/#/pet/addPet in a Chromium-based browser. - Open the developer tools (F12) and select the **Network** tab. - Select only **Fetch/XHR**. - Click **Execute** in the web page to start a network call to the API to add a pet. - In the developer tools, click the `pet` line that appears to see the details of the request and response. ![XHR](/assets/openapi/guide/xhr.png) - Also execute a pet update (PUT). (In reality, you want to call every endpoint in your API, but we don't need to do that now.) - In the network tab, click the download arrow icon in the top row. - Save an HAR file (HTTP archive) to your desktop. Several tools will convert HAR to an OpenAPI schema. We'll use APIGit. - Browse to https://app.apigit.com. - Create an account. - Create a repository named `pet` (any name will do). - In the sidebar, browse to **API Documents**, then click the plus sign to add a document. - Upload the HAR file you saved previously. - Set the path name to `pet.yaml`. - Click **Create**. - Download the YAML file. This is your OpenAPI schema. ![APIGit](/assets/openapi/guide/apigit.png) You can view the schema in a text editor or in a more visually descriptive way in an OpenAPI editor. - Browse to https://editor-next.swagger.io. - Import your YAML file. ![Swagger editor](/assets/openapi/guide/editornext.png) The editor will show errors and duplicated objects that could be moved into the components section and shared. [Other network capture tools](https://tools.openapis.org/categories/all.html) are available. The all-in-one OpenAPI DevTools plugin from Chrome Web Store offers network recording and OpenAPI schema generation. ## Comment annotation Once you know what all the operations and parameters for your API should be in OpenAPI terms – either by using the generator as discussed in the previous section or by reading the list of OpenAPI data types – you can annotate your existing code with comments. A generator will then use the comments to output an OpenAPI schema. As mentioned in the introduction, we recommend using attribute annotations instead of comment annotations whenever possible. If your server framework doesn't support attribute annotations, this approach is an alternative option. You should consider whether to continue using your existing framework or switch to one with native support for OpenAPI. In the long term, adding and maintaining annotations might be more or less work than rewriting and maintaining OpenAPI-aware code. Comment annotation, like attribute annotation and using an OpenAPI meta-framework, depends on the programming language you use. ### Example comment annotation in Go The tool used here is [go-swagger3](https://github.com/parvez3019/go-swagger3). - In any folder on your computer, create a folder called `src`. - Create a file in `src` called `main.go` and add the content below to it. ```go package main import ( "encoding/json" "fmt" "log" "net/http" ) type RequestBody struct { Name string `json:"name"` } func main() { mux := http.NewServeMux() mux.HandleFunc("/hello", helloHandler) log.Println("Server started on port 8080") log.Fatal(http.ListenAndServe(":8080", mux)) } func helloHandler(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodPost { http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) return } var requestBody RequestBody err := json.NewDecoder(r.Body).Decode(&requestBody) if err != nil { http.Error(w, "Bad request", http.StatusBadRequest) return } response := fmt.Sprintf("Hello %s", requestBody.Name) w.Header().Set("Content-Type", "text/plain") w.Write([]byte(response)) } ``` This minimal web service has a single POST endpoint, `/hello` that says hello to the name sent as a string parameter called `name`. - Build and run the service with the commands below to see that it works. You don't need to install Go on your machine, but you do need Docker. Docker will run Go in a container in a security sandbox so it has no access to the rest of your files. You then call the service in the terminal with curl. ```sh docker run --rm -v $(pwd)/src:/app -w /app golang:1.23-alpine3.19 /bin/sh -c "go mod init helloworld && go mod vendor && go build" docker run --rm -p 65000:8080 -v $(pwd)/src:/app -w /app golang:1.23-alpine3.19 ./helloworld # In a new terminal curl -X POST -H "Content-Type: application/json" -d '{"name":"John"}' http://localhost:65000/hello # Output # Hello John ``` Now let's update the web service with some OpenAPI comments. - Update `main.go`, adding the following at the top of the file: ```go // @Version 1.0.0 // @Title Hello API // @Description Testing Go OpenAPI frameworks package main ... ``` - Add comments above the operation: ```go // @Title Hello // @Description Say hi to the name given // @Param name body RequestBody true "Any name" // @Success 200 {string} string "Responds with 'Hello {name}'" // @Route /helloHandler [post] func helloHandler(w http.ResponseWriter, r *http.Request) { ... ``` To generate a schema from this file, you can either install go-swagger3 in the standard Go container you just used or use the dedicated go-swagger3 Docker image. The first option requires redownloading the Go modules each time, as they are not stored in your project `vendor` folder. Run one of the two commands below. ```sh docker run --rm -v $(pwd)/src:/app -w /app golang:1.23-alpine3.19 /bin/sh -c "go install github.com/parvez3019/go-swagger3@latest && /go/bin/go-swagger3 --main-file-path /app/main.go --module-path . --output oas.json --schema-without-pkg --generate-yaml true" # OR use dedicated docker image docker run -t --rm -v $(pwd)/src:/app -w /app parvez3019/go-swagger3:latest --main-file-path /app/main.go --module-path . --output oas.json --schema-without-pkg --generate-yaml true ``` In your `src` folder, you'll now have a schema called `oas.yml`: ```yml components: schemas: RequestBody: properties: name: type: string type: object info: description: Say hi to the name given title: Hello version: 1.0.0 openapi: 3.0.0 paths: /helloHandler: post: description: ' Say hi to the name given' requestBody: content: application/json: schema: $ref: '#/components/schemas/RequestBody' required: true responses: "200": content: application/json: schema: type: string description: Responds with 'Hello {name}' summary: Hello servers: - description: Default Server URL url: / ``` ## Attribute annotation All the considerations raised about comment annotations in the previous section also apply to attribute annotations, except that attribute annotations are safer than comment annotations as they can be parsed by a compiler. ### Example attribute annotation with an OpenAPI-aware framework in Go To demonstrate attribute annotation, we'll use a framework called [Huma](https://huma.rocks/), which [supports many Go routers](https://huma.rocks/features/bring-your-own-router). Unlike a simple annotation plugin that sits above your existing code, Huma requires you to alter your operations to use the Huma context object. While Huma doesn't support the standard Go `net/http` router, it does support the more complex `mux` handler. If you'd like to follow along, use the existing `main.go` file from the previous section. First, install Huma: ```sh docker run --rm -v $(pwd)/src:/app -w /app golang:1.23-alpine3.19 go get github.com/danielgtaylor/huma/v2 github.com/danielgtaylor/huma/v2/adapters/humago ``` Update `main.go` to use Huma with the changes below. ```go package main import ( // "encoding/json" "fmt" "log" "net/http" "context" "github.com/danielgtaylor/huma/v2" // NEW "github.com/danielgtaylor/huma/v2/adapters/humago" ) type RequestBody struct { // Name string `json:"name"` Body struct { Name string `json:"name" maxLength:"30" example:"world" doc:"Name to greet"` } } //NEW type ResponseBody struct { Body struct { Message string `json:"message"` } } func main() { mux := http.NewServeMux() api := humago.New(mux, huma.DefaultConfig("Hello API", "1.0.0")) // NEW huma.Post(api, "/hello", helloHandler) // NEW log.Println("Server started on port 8080") log.Fatal(http.ListenAndServe(":8080", mux)) } // NEW func helloHandler(ctx context.Context, input *RequestBody) (*ResponseBody, error) { response := &ResponseBody { Body: struct { Message string `json:"message"`}{ Message: fmt.Sprintf("Hello %s", input.Body.Name), }, } return response, nil } ``` Note that the code above has changed the request and response types to have a `Body` section. Huma returns a schema object in the response at the same level as `Body`. The `main` function no longer uses the router in the same way, but now uses Huma to declare routes. Now run the server. ```sh docker run --rm -p 65000:8080 -v $(pwd)/src:/app -w /app golang:1.23-alpine3.19 /bin/sh -c "go build && ./helloworld" # In a new terminal: curl -X POST -H "Content-Type: application/json" -d '{"name":"John"}' http://localhost:65000/hello # Output # {"$schema":"http://localhost:65000/schemas/ResponseBodyBody.json","message":"Hello John"} ``` Browse to http://localhost:65000/docs#/operations/post-hello to see the generated schema in a Swagger-like viewer. ![Huma OpenAPI documentation](/assets/openapi/guide/huma.png) ## Using an OpenAPI meta-framework The final option is to manually rewrite your server router in a meta-framework, and use that framework to generate a schema and new server code. For Go, this framework is called Goa. Speakeasy has an article demonstrating the process [here](/openapi/frameworks/goa). In brief, the code you'll write is Go, but it is neither normal server code nor OpenAPI in YAML. It looks like the Go description of a schema below. ```go var _ = Service("order", func() { Description("A waiter that brings drinks.") Method("tea", func() { Description("Order a cup of tea.") Payload(func() { Field(1, "isGreen", Boolean, "Whether to have green tea instead of normal.") Field(2, "numberSugars", Int, "Number of spoons of sugar.") Field(3, "includeMilk", Boolean, "Whether to have milk.") }) Result(String) HTTP(func() { Meta("openapi:tag:Drink operations") POST("/tea") }) GRPC(func() { }) }) Files("/openapi.json", "./gen/http/openapi.json") }) ``` Using an OpenAPI meta-framework to generate a schema is highly dependent on the programming language you use, as most languages don't have tools like Goa available (see the list of tools in the next section). A meta-framework is a great tool for maintaining server code separately from your schema (unlike using annotations), with the added advantage that it allows business analysts to understand and edit API contracts without needing to know a programming language in detail. It also makes the code to be maintained isolated and small, compared to annotating your entire server codebase. The disadvantage of this approach is that you will be heavily dependent on the framework you choose, and that it's regularly maintained to fix bugs in generation and stay current with changes to OpenAPI. You will also be choosing a tool that's built on top of another tool. It might be easier for your team to stick with a popular web framework and simple OpenAPI annotations. Evaluate the framework you are considering and the quality of its outputs in a short test to decide if you like this approach. ## Tools available in each language Now that you have seen the options you can choose from to generate an OpenAPI schema, you need to see if the tools you need are available for your language. In this final section, we list all the best tools we have found, ignoring repositories that haven't been updated in a year. ### C#, F#, and .NET The .NET framework has two libraries you can use to generate OpenAPI schemas: [OpenAPI.NET](https://github.com/microsoft/openapi.net) and [NSwag](https://github.com/ricosuter/nswag). Microsoft has official tutorials on both: - [Generate OpenAPI documents with the `Microsoft.AspNetCore.OpenApi` package](https://learn.microsoft.com/en-us/aspnet/core/fundamentals/openapi/aspnetcore-openapi?view=aspnetcore-8.0&tabs=visual-studio%2Cminimal-apis) - [Get started with NSwag and ASP.NET Core](https://learn.microsoft.com/en-us/aspnet/core/tutorials/getting-started-with-nswag?view=aspnetcore-8.0&tabs=visual-studio) The `AspNetCore.OpenApi` package allows you to add attributes to your code. NSwag can generate client and server code, too. ### Go [go-swagger3](https://github.com/parvez3019/go-swagger3) creates OpenAPI schemas for Go code based on text comments. [Huma](https://huma.rocks) is a combination of annotations and an OpenAPI-aware framework that allows you to add attributes to your API to generate a schema and serve documentation for it. [Fuego](https://go-fuego.github.io/fuego/docs/guides/openapi) is an OpenAPI-aware web framework. The [`swaggest/rest`](https://github.com/swaggest/rest) package, based on the Chi web framework, and [GoAPI](https://github.com/hvuhsg/goapi) offer similar functionality. [Goa](https://github.com/goadesign/goa?tab=readme-ov-file) is a meta-framework that allows you to write an API design in Go code, which it then compiles into web server code and an OpenAPI schema. [ogen](https://ogen.dev/docs/intro) generates data types and server and client code from an OpenAPI schema, but not an OpenAPI schema. ### JavaScript JavaScript does not have static typing, but you can add type hints in text comments using [JSDoc](https://jsdoc.app/about-getting-started). [swagger-jsdoc](https://github.com/Surnet/swagger-jsdoc) will convert your JSDoc annotations to an OpenAPI schema. [hapi-swagger](https://github.com/hapi-swagger/hapi-swagger) adds OpenAPI features to the hapi web framework by using JSON descriptions of routes. ### TypeScript [Tsoa](https://tsoa-community.github.io/docs/getting-started.html#configuring-tsoa-and-typescript) is similar to Goa — a meta-framework in TypeScript that will output server code and an OpenAPI schema. [swagger-docs](https://github.com/AmishFaldu/swagger-docs/wiki/Getting-Started) creates an OpenAPI schema for you from TypeScript attributes. ### PHP [OpenAPI PHP Attributes Generator](https://github.com/uderline/openapi-php-attributes) is a code-first tool that generates an OpenAPI schema from annotations. [Swagger-PHP](https://zircote.github.io/swagger-php/) generates an OpenAPI schema from code attributes or comment annotations. If you're using Laravel, you can use [Scramble](https://scramble.dedoc.co) to generate an OpenAPI schema without needing to add annotations. [Scribe](https://scribe.knuckles.wtf) is similar. [API Platform](https://api-platform.com/), which works with Laravel and Symfony, provides OpenAPI features using your existing code, extended by attributes. [PSX](https://phpsx.org/docs/intro) is an OpenAPI-aware web framework. ### Python [drf-spectacular](https://drf-spectacular.readthedocs.io/en/latest/readme.html) automatically generates an OpenAPI schema from your Django code. You can add more information to your code with the `@extend_schema` attribute. [FastAPI](https://fastapi.tiangolo.com) is an OpenAPI-aware web framework. ### Rust [Poem](https://docs.rs/poem-openapi/latest/poem_openapi/) is the only Rust web framework that is OpenAPI-aware. Poem automatically generates an OpenAPI schema for you and serves it through the Swagger UI at your site `/docs`. The three most popular web frameworks, axum, Actix Web, and Rocket, don't support OpenAPI yet, though they are working on it. For these, you should add OpenAPI attribute annotations with [utoipa](https://github.com/juhaku/utoipa). The Axum team has been working to elegantly integrate with utoipa. Take a look at an example [here](https://github.com/juhaku/utoipa/blob/master/examples/axum-utoipa-bindings/src/main.rs). ### Java For frameworks that support the Jakarta RESTful Web Services specification (JAX-RS), formerly Java API for RESTful Web Services, you can use [Swagger Core](https://github.com/swagger-api/swagger-core/wiki/Swagger-2.X---Getting-started) to automatically generate an OpenAPI schema from code annotations. [Swagger Parser](https://github.com/swagger-api/swagger-parser) will generate Java objects when given an OpenAPI schema. [guardrail](https://github.com/guardrail-dev/guardrail) will generate server code for you from your OpenAPI schema and supports Spring and Dropwizard. [openapi-processor](https://openapiprocessor.io/oap/home.html) will generate Java objects and server code interfaces in Spring or Micronaut, given an OpenAPI schema. ### Scala If you're working in Scala, you can use the tools described in the Java section or the Scala-specific tools described below. [endpoints4s](https://endpoints4s.github.io/quick-start.html) is a meta-framework that describes your API in code. You reference this code as a shared project in your client and server code, then implement the functions the API description specifies. endpoints4s will also generate an OpenAPI schema for you. Note that endpoints4s will not generate any code for you. [SBT OpenApi Schema Codegen](https://github.com/eikek/sbt-openapi-schema) will generate the `/components/schema` types from your OpenAPI schema in Scala. [Guardrail](https://github.com/guardrail-dev/guardrail) is a schema-first tool that will generate server code for you from your OpenAPI schema. It supports Akka, Dropwizard, and http4s. ### Kotlin [Fabrikt](https://github.com/cjbooms/fabrikt) is a schema-first tool that generates data classes and interfaces for server and client code. ## The schema-first workflow The alternative to a code-first approach is schema-first. If you treat your OpenAPI schema as your principal source after writing it or generating it from your code, and generate server code from it thereafter, then your code and schema will always be synchronized. While this approach is similar to the meta-framework approach, it provides a schema that's easier to read for non-programmers, though it may not be able to generate the framework's more specific features. A schema-first approach also allows you to add detailed text descriptions to help clients understand what your service endpoint does, which you may not be able to do in code. Generating server code doesn't mean that you have to overwrite your existing code. You can use the generated code merely as an interface, or contract, to call your underlying code and check that all its requests and responses are of the expected types. ## Further reading While there are a few tools listed above that do more than generate a schema from code, there are hundreds of OpenAPI tools and services available. Tools that validate code against schemas, create contracts, generate server code from a schema, and create and run tests. To see what's available, visit: - https://tools.openapis.org/categories/all.html # How to generate an OpenAPI document with Django and Django REST framework Source: https://speakeasy.com/openapi/frameworks/django import { Callout } from "@/mdx/components"; OpenAPI is a tool for defining and sharing REST APIs, and Django can be paired with Django REST framework to build such APIs. This guide walks you through generating an OpenAPI document from a Django project and using it to create SDKs with Speakeasy, covering the following steps: 1. Setting up a simple Django REST API with `djangorestframework` 2. Integrating `drf-spectacular` 3. Creating the OpenAPI document to describe the API 4. Customizing the OpenAPI schema 5. Using the Speakeasy CLI to create an SDK based on the schema 6. Integrating SDK creation into CI/CD workflows ## Requirements This guide assumes you have a basic understanding of Django project structure and how REST APIs work. You will also need the following installed on your machine: - Python version 3.8 or higher - Django You can install Django using the following command: ```bash filename="Terminal" pip install django ``` - Django REST Framework You can install Django REST Framework using the following command: ```bash filename="Terminal" pip install djangorestframework ``` ## Example Django REST API repository The source code for the completed example is available in the [**Speakeasy Django example repository**](https://github.com/speakeasy-api/django-openapi-example). The example repository contains all the code covered in this guide. You can clone it and follow along with the tutorial or use it as a reference to add to your own Django project. --- ## Creating the OpenAPI document to describe an API To better understand the process of generating an OpenAPI document with Django, let's start by inspecting some simple CRUD endpoints for an online library, along with a `Book` class and a serializer for the data. ### Models, serializers, and views Let's look at the key components of our Django REST API: ### Book Model First, let's examine the `books/models.py` file, which contains a `Book` model with validation fields: ```python filename="models.py" from django.db import models class Book(models.Model): title = models.CharField(max_length=100) author = models.CharField(max_length=100) published_year = models.IntegerField() ``` ### Book Serializer Next, let's look at the `books/serializers.py` file, which defines a `BookSerializer` for serializing and deserializing Book data: ```python filename="serializers.py" from rest_framework import serializers from .models import Book class BookSerializer(serializers.ModelSerializer): class Meta: model = Book fields = ['id', 'title', 'author', 'published_year'] ``` ### Book Views The `books/views.py` file contains a `BookViewSet` that handles CRUD operations for the Book model: ```python filename="views.py" from rest_framework import viewsets, status from rest_framework.decorators import action from rest_framework.response import Response from .models import Book from .serializers import BookSerializer class BookViewSet(viewsets.ModelViewSet): """ API endpoint that allows books to be viewed or edited. """ queryset = Book.objects.all() serializer_class = BookSerializer @action(detail=True, methods=['get']) def author_books(self, request, pk=None): """ Returns all books written by the same author as the specified book. """ try: book = self.get_object() except Book.DoesNotExist: return Response( {"error": "Book not found"}, status=status.HTTP_404_NOT_FOUND ) author_books = Book.objects.filter(author=book.author).exclude(id=book.id) serializer = self.get_serializer(author_books, many=True) return Response(serializer.data) ``` This code defines a simple Django REST API with CRUD operations for the `Book` model. The `BookViewSet` provides a way to interact with the `Book` model through the API. It also contains a custom action called `author_books` that retrieves all books by the same author. ### URL Configuration The `books/urls.py` file maps the `BookViewSet` to the `/books` endpoint: ```python filename="urls.py" from django.urls import path, include from rest_framework.routers import DefaultRouter from .views import BookViewSet router = DefaultRouter() router.register(r'books', BookViewSet) urlpatterns = [ path('', include(router.urls)), ] ``` And in the `books_project/urls.py` file, the router is included in the main Django URL configuration: ```python filename="urls.py" from django.contrib import admin from django.urls import path, include from drf_spectacular.views import SpectacularAPIView, SpectacularSwaggerView urlpatterns = [ path('admin/', admin.site.urls), path('api/schema/', SpectacularAPIView.as_view(), name='schema'), path('swagger/', SpectacularSwaggerView.as_view(url_name='schema'), name='swagger-ui'), path('api/', include('books.urls')), ] ``` ## Integrate `drf-spectacular` Django no longer supports the built-in OpenAPI document generation, so we'll use the `drf-spectacular` package to generate the OpenAPI document. Run the following to install `drf-spectacular`: ```bash filename="Terminal" pip install drf-spectacular ``` ### Setting up drf-spectacular First, open the `books_project/settings.py` file to see how `drf-spectacular` is configured: ```python filename="settings.py" INSTALLED_APPS = [ 'django.contrib.admin', 'django.contrib.auth', 'django.contrib.contenttypes', 'django.contrib.sessions', 'django.contrib.messages', 'django.contrib.staticfiles', # Third party apps 'rest_framework', 'drf_spectacular', # Local apps 'books', ] ``` Adding `'drf_spectacular'` to the `INSTALLED_APPS` list enables OpenAPI document generation for your Django project. Next, check the `REST_FRAMEWORK` configuration object, which sets the schema class used to create the OpenAPI document: ```python filename="settings.py" REST_FRAMEWORK = { 'DEFAULT_SCHEMA_CLASS': 'drf_spectacular.openapi.AutoSchema', } ``` The `SPECTACULAR_SETTINGS` dictionary contains additional settings for OpenAPI document generation that you can customize to fit your project: ```python filename="settings.py" SPECTACULAR_SETTINGS = { 'TITLE': 'Library API', 'DESCRIPTION': 'A simple API for managing books in a library', 'VERSION': '1.0.0', 'SERVE_INCLUDE_SCHEMA': False, 'COMPONENT_SPLIT_REQUEST': True, 'SERVERS': [ {'url': 'http://localhost:8000', 'description': 'Local Development server'}, {'url': 'https://api.example.com', 'description': 'Production server'}, ], 'TAGS': [ {'name': 'books', 'description': 'Book operations'}, ], 'EXTENSIONS_TO_SCHEMA_FUNCTION': lambda generator, request, public: { 'x-speakeasy-retries': { 'strategy': 'backoff', 'backoff': { 'initialInterval': 500, 'maxInterval': 60000, 'maxElapsedTime': 3600000, 'exponent': 1.5, }, 'statusCodes': ['5XX'], 'retryConnectionErrors': True, } } } ``` In the `books_project/urls.py` file, the OpenAPI schema and Swagger UI endpoints are added alongside the `api/` endpoint: ```python filename="urls.py" from django.contrib import admin from django.urls import path, include from drf_spectacular.views import SpectacularAPIView, SpectacularSwaggerView urlpatterns = [ path('admin/', admin.site.urls), path('api/schema/', SpectacularAPIView.as_view(), name='schema'), path('swagger/', SpectacularSwaggerView.as_view(url_name='schema'), name='swagger-ui'), path('api/', include('books.urls')), ] ``` ### Apply migrations and run the server To inspect and interact with the OpenAPI document, you need to apply database migrations and run the development server. Apply database migrations: ```bash filename="Terminal" python manage.py makemigrations python manage.py migrate ``` Run the development server: ```bash filename="Terminal" python manage.py runserver ``` You can now access the API and documentation: * Visit `http://127.0.0.1:8000/api/books/` to interact with the book API. * Visit `http://127.0.0.1:8000/swagger/` for Swagger documentation. ### OpenAPI document generation Now that we understand our Django REST API, we can generate the OpenAPI document using `drf-spectacular` with the following command: ```bash filename="Terminal" python manage.py spectacular --file openapi.yaml ``` ### Exploring the Generated OpenAPI Document Running the command generates an OpenAPI document in the `openapi.yaml` file. Let's look at some key sections: #### Document Header The beginning of the document contains general information about the API: ```yaml filename="openapi.yaml" openapi: 3.0.3 info: title: Library API description: A simple API for managing books in a library version: 1.0.0 contact: {} components: schemas: Book: type: object properties: id: type: integer readOnly: true # More properties follow... ``` #### Settings Influence on Generated Document The values in `SPECTACULAR_SETTINGS` directly influence the OpenAPI document generation. For example, the title, description, and version in the settings: ```python filename="settings.py" SPECTACULAR_SETTINGS = { 'TITLE': 'Library API', 'DESCRIPTION': 'A simple API for managing books in a library', 'VERSION': '1.0.0', # Other settings... } ``` These values appear in the OpenAPI document: ```yaml filename="openapi.yaml" info: title: Library API description: A simple API for managing books in a library version: 1.0.0 ``` #### Server Information The server URLs specified in the settings appear in the document as well: ```yaml filename="openapi.yaml" servers: - url: http://localhost:8000 description: Local Development server - url: https://api.example.com description: Production server ``` #### Model Parameters The fields we defined in our Django models are also reflected in the OpenAPI document. For example, the `Book` model fields: ```python filename="models.py" class Book(models.Model): title = models.CharField(max_length=100) author = models.CharField(max_length=100) published_year = models.IntegerField() ``` These fields appear in the OpenAPI document's schema definitions: ```yaml filename="openapi.yaml" Book: type: object properties: id: type: integer readOnly: true title: type: string maxLength: 100 author: type: string maxLength: 100 published_year: type: integer required: - title - author - published_year ``` The OpenAPI document captures all the essential information about our API, including endpoints, parameters, request bodies, responses, and schemas. This document can then be used to generate client SDKs or API documentation. ## OpenAPI document customization The OpenAPI document generated by `drf-spectacular` may not be detailed enough for all use cases. Fortunately, it can be customized to better serve information about your API endpoints. You can add descriptions, tags, examples, and more to make the documentation more informative and user-friendly. In the [customized](https://github.com/speakeasy-api/django-openapi-example/tree/customized) branch of the example repository, you can find a customized OpenAPI document that demonstrates the available options for modifying your generated document. The `drf-spectacular` package provides decorators to directly modify the schema for your views and viewsets. - `@extend_schema_view`: Allows customization of all methods in a viewset. - `@extend_schema`: Allows customization of individual methods or actions. ### Customizing the API Schema Let's explore how to enhance the OpenAPI document by customizing the schema of the `BookViewSet`. Here's an updated version of the `books/views.py` file with added annotations: ```python filename="views.py" from rest_framework import viewsets, status from rest_framework.decorators import action from rest_framework.response import Response from drf_spectacular.utils import extend_schema, extend_schema_view, OpenApiParameter, OpenApiExample, OpenApiResponse from .models import Book from .serializers import BookSerializer # Use extend_schema_view to customize the entire viewset @extend_schema_view( list=extend_schema( summary="List all books", description="Get a list of all books in the library.", responses={ 200: BookSerializer(many=True) }, tags=["books"], ), retrieve=extend_schema( summary="Get a specific book", description="Retrieve details for a specific book by its ID.", responses={ 200: BookSerializer, 404: OpenApiResponse(description="Book not found"), }, tags=["books"], ), # Other methods... ) class BookViewSet(viewsets.ModelViewSet): """ API endpoint that allows books to be viewed or edited. """ queryset = Book.objects.all() serializer_class = BookSerializer # Use extend_schema to customize a specific action @extend_schema( summary="Find books by the same author", description="Returns all books written by the same author as the specified book.", responses={ 200: BookSerializer(many=True), 404: OpenApiResponse( description="Book not found", examples=[ OpenApiExample( "Error Response", value={"error": "Book not found"}, status_codes=["404"], ) ] ) }, tags=["books", "authors"], parameters=[ OpenApiParameter( name="sort", description="Sort order for the books", required=False, type=str, enum=["title", "published_year"], ), ], examples=[ OpenApiExample( "Book list example", value=[ { "id": 1, "title": "The Great Gatsby", "author": "F. Scott Fitzgerald", "published_year": 1925 }, { "id": 2, "title": "Tender Is the Night", "author": "F. Scott Fitzgerald", "published_year": 1934 } ], response_only=True, status_codes=["200"], ) ], extensions={ "x-speakeasy-retries": { "strategy": "backoff", "backoff": { "initialInterval": 500, "maxInterval": 60000, "maxElapsedTime": 3600000, "exponent": 1.5, }, "statusCodes": ["5XX"], "retryConnectionErrors": True, } } ) @action(detail=True, methods=['get']) def author_books(self, request, pk=None): # Implementation details... pass ``` #### Using @extend_schema_view The `@extend_schema_view` decorator allows you to customize all methods in a viewset at once. In our example, we're customizing the `list` and `retrieve` operations with summaries, descriptions, and response details. This will appear in the generated OpenAPI document as: ```yaml filename="openapi.yaml" paths: /api/books/: get: operationId: books_list summary: List all books description: Get a list of all books in the library. parameters: # Standard parameters here responses: '200': description: '' content: application/json: schema: type: array items: $ref: '#/components/schemas/Book' tags: - books ``` #### Customizing Individual Actions with @extend_schema For specific actions like `author_books`, we use the `@extend_schema` decorator to add detailed documentation: ```python filename="views.py" @extend_schema( summary="Find books by the same author", description="Returns all books written by the same author as the specified book.", # Other options... ) @action(detail=True, methods=['get']) def author_books(self, request, pk=None): # Implementation... ``` This will generate OpenAPI documentation for this endpoint: ```yaml filename="openapi.yaml" /api/books/{id}/author_books/: get: operationId: books_author_books summary: Find books by the same author description: Returns all books written by the same author as the specified book. parameters: - name: id in: path required: true schema: type: integer - name: sort in: query description: Sort order for the books required: false schema: type: string enum: - title - published_year responses: '200': content: application/json: schema: type: array items: $ref: '#/components/schemas/Book' examples: Book list example: value: - id: 1 title: The Great Gatsby author: F. Scott Fitzgerald published_year: 1925 - id: 2 title: Tender Is the Night author: F. Scott Fitzgerald published_year: 1934 '404': description: Book not found content: application/json: examples: Error Response: value: error: Book not found tags: - books - authors ``` #### Adding Custom Parameters You can add custom query parameters to your endpoints using `OpenApiParameter`: ```python filename="views.py" parameters=[ OpenApiParameter( name="sort", description="Sort order for the books", required=False, type=str, enum=["title", "published_year"], ), ] ``` #### Adding Examples Examples help API users understand the expected responses: ```python filename="views.py" examples=[ OpenApiExample( "Book list example", value=[ { "id": 1, "title": "The Great Gatsby", "author": "F. Scott Fitzgerald", "published_year": 1925 }, # More examples... ], response_only=True, status_codes=["200"], ) ] ``` #### Adding Retry Logic You can add retry configuration at the global level in `settings.py`: ```python filename="settings.py" 'EXTENSIONS_TO_SCHEMA_FUNCTION': lambda generator, request, public: { 'x-speakeasy-retries': { 'strategy': 'backoff', 'backoff': { 'initialInterval': 500, 'maxInterval': 60000, 'maxElapsedTime': 3600000, 'exponent': 1.5, }, 'statusCodes': ['5XX'], 'retryConnectionErrors': True, } } ``` Or apply it to specific endpoints using the `extensions` parameter in `@extend_schema`: ```python filename="views.py" extensions={ "x-speakeasy-retries": { "strategy": "backoff", "backoff": { "initialInterval": 500, "maxInterval": 60000, "maxElapsedTime": 3600000, "exponent": 1.5, }, "statusCodes": ["5XX"], "retryConnectionErrors": True, } } ``` In summary, the `drf-spectacular` package provides a variety of ways to customize the OpenAPI document for your Django REST API. You can use decorators, tags, descriptions, parameters, fields, examples, and global settings to modify the document according to your requirements. - **Decorators (@extend_schema and @extend_schema_view):** Customize individual methods or entire views. - **Tags and descriptions:** Organize endpoints for better readability. - **Parameters:** Define custom parameters using `OpenApiParameter`. - **OpenAPI components:** Use `OpenApiExample` to provide reusable components or examples. - **Global settings (`SPECTACULAR_SETTINGS`):** Modify the global behavior of `drf-spectacular`. For more information about customizing the OpenAPI schema with `drf-spectacular`, refer to the official [`drf-spectacular` documentation](https://drf-spectacular.readthedocs.io/en/latest/). ## Creating SDKs for a Django REST API To create a Python SDK for the Django REST API, run the following command: ```bash filename="Terminal" speakeasy quickstart ``` Follow the onscreen prompts to provide the configuration details for your new SDK, such as the name, schema location, and output path. When prompted, enter `openapi.yaml` for the OpenAPI document location, select a language, and generate. ## Add SDK generation to your GitHub Actions The Speakeasy [`sdk-generation-action`](https://github.com/speakeasy-api/sdk-generation-action) repository provides workflows for integrating the Speakeasy CLI into your CI/CD pipeline, so that your SDKs are recreated whenever your OpenAPI document changes. You can set up Speakeasy to automatically push a new branch to your SDK repositories for your engineers to review before merging the SDK changes. For an overview of how to set up automation for your SDKs, see the Speakeasy [SDK Generation Action and Workflows](/docs/speakeasy-reference/workflow-file) documentation. ## SDK customization Explore the effects of your newly generated OpenAPI document on the SDK created by Speakeasy. After creating your SDK with Speakeasy, you will find a new directory containing the generated SDK code. Let's explore this code a bit further. These examples assume a Python SDK named `books-python` was generated from the example Django project above. Edit any paths to reflect your environment if you want to follow in your own project. ### Exploring the Generated SDK After generating your SDK with Speakeasy, let's explore the key files and how they relate to your OpenAPI document. #### The Book Class Navigate to the `books-python/src/books` directory to find the generated SDK code. The `book.py` file contains the `Book` class that corresponds to your Django model: ```python filename="book.py" from __future__ import annotations import dateutil.parser from datetime import datetime from marshmallow import fields from typing import Any, Dict, List, Optional, Union from dataclasses import dataclass from dataclasses_json import dataclass_json from books import utils @dataclass_json @dataclass class Book: """A book in the library API""" id: Optional[int] = None title: str = None author: str = None published_year: int = None def unmarshal( obj: Union[Dict[str, Any], str] ) -> Book: """ Unmarshals a Book from a dictionary or a JSON string. """ if isinstance(obj, str): obj = utils.loads(obj) return Book( id=obj.get("id"), title=obj.get("title"), author=obj.get("author"), published_year=obj.get("published_year"), ) ``` #### API Client Code The `api.py` file contains methods that call the web API from an application using the SDK: ```python filename="api.py" class BooksSDK: """SDK for accessing the Library API""" def __init__( self, security: Optional[shared.Security] = None, retries: Optional[utils.RetryConfig] = None, server_url: Optional[str] = None, server_idx: Optional[int] = None, client_config: Optional[client.ClientConfig] = None, ): """Initialize the SDK client""" if server_url is not None: self.server_url = server_url elif server_idx is not None: self.server_url = utils.SERVERS[server_idx] else: self.server_url = utils.SERVERS[0] self.client_config = client_config self.security = security self.retries = retries ``` Notice several important parameters: 1. The `server_url` parameter, which comes from the `SERVERS` key in your `SPECTACULAR_SETTINGS`: ```python filename="api.py" self.server_url = utils.SERVERS[0] # Default to first server ``` 2. The `retries` parameter, which is generated from your retry configuration: ```python filename="api.py" self.retries = retries ``` #### Making API Requests These parameters are used to build requests to your API endpoints: ```python filename="api.py" def list_books( self, request_options: Optional[utils.RequestOptions] = None, ) -> operations.BooksListResponse: """List all books in the library""" base_url = self.server_url url = utils.generate_url(base_url, "/api/books/") headers = {} headers["Accept"] = "application/json" client = self.get_client() retry_config = request_options.retry_config if request_options and request_options.retry_config is not None else self.retries return utils.retry( lambda: client.request("GET", url, headers=headers), retry_config ) ``` #### Retry Logic Implementation The SDK includes a retry implementation based on your OpenAPI extensions: ```python filename="retries.py" class RetryConfig: """Configuration for retry behavior""" def __init__( self, strategy: str = None, backoff: Optional[BackoffStrategy] = None, retry_connection_errors: bool = False, status_codes: Optional[List[str]] = None, ): """Initialize retry configuration""" self.strategy = strategy self.backoff = backoff self.retry_connection_errors = retry_connection_errors self.status_codes = status_codes # The implementation of the retry logic def retry( callback: Callable[[], Response], config: Optional[RetryConfig] = None, ) -> Response: """ Retries the given callback based on the retry configuration. """ if config is None or config.strategy != "backoff" or config.backoff is None: return callback() retry_attempt = 0 status_codes = _parse_status_codes(config.status_codes) while True: try: response = callback() # Check if we should retry based on the status code if response.status_code in status_codes: if retry_attempt >= config.backoff.max_retries: return response _sleep_with_jitter(config.backoff, retry_attempt) retry_attempt += 1 continue return response except requests.exceptions.ConnectionError as e: if not config.retry_connection_errors or retry_attempt >= config.backoff.max_retries: raise e _sleep_with_jitter(config.backoff, retry_attempt) retry_attempt += 1 ``` This retry logic directly reflects the configuration you provided in the `x-speakeasy-retries` extension in your OpenAPI document, ensuring consistent behavior between your API documentation and the generated SDK. ## Summary In this guide, we showed you how to generate an OpenAPI document for a Django API and use Speakeasy to create an SDK based on the OpenAPI document. The step-by-step instructions included adding relevant tools to the Django project, generating an OpenAPI document, enhancing it for improved creation, using Speakeasy OpenAPI extensions, and interpreting the basics of the generated SDK. We also explored automating SDK generation through CI/CD workflows and improving API operations. # How to generate an OpenAPI document with ElysiaJS Source: https://speakeasy.com/openapi/frameworks/elysia import { Callout } from "@/mdx/components"; This guide walks you through generating an OpenAPI document for an [ElysiaJS](https://elysiajs.com/) API and using Speakeasy to create an SDK based on the generated document. Here's what we'll do: 1. Add a Swagger endpoint, which uses Scalar UI, to an Elysia Bun app using the Elysia Swagger plugin. 2. Improve the OpenAPI document to prepare it for code generation. 3. Convert the JSON OpenAPI document to YAML. 4. Use the Speakeasy CLI to generate an SDK based on the OpenAPI document. 5. Add a Speakeasy OpenAPI extension to improve the generated SDK. We'll also take a look at how you can use the generated SDK. Your Elysia project might not be as simple as our example app, but the steps below should translate well to any Elysia project. ## The OpenAPI generation pipeline The Elysia [Swagger plugin](https://github.com/elysiajs/elysia-swagger) generates a Swagger API documentation endpoint. By default, Elysia uses the OpenAPI Specification and [Scalar UI](https://scalar.com/), an open-source interactive document UI for OpenAPI. We'll first add the Swagger plugin to an existing Elysia app. Then, we'll improve the plugin-generated OpenAPI document according to Speakeasy [best practices](https://www.speakeasy.com/docs/best-practices). The quality of an OpenAPI document determines the quality of the SDKs and documentation it's used to create. Next, we'll use Speakeasy to generate an SDK based on the OpenAPI document. Finally, we'll use a simplified example to demonstrate how to use the generated SDK and how to add SDK creation to a CI/CD pipeline so that Speakeasy automatically generates fresh SDKs whenever your Elysia API changes in the future. ## Requirements This guide assumes that you have an existing Elysia app and basic familiarity with Elysia. If you don't have an Elysia app or if you want to follow the guide step by step, you can clone the [Speakeasy ElysiaJS example repo](https://github.com/speakeasy-api/elysia-openapi-example) to access the example code used in this tutorial. The `initial-app` branch contains the initial state of the app that we'll use to start this tutorial. The following should be installed on your machine: - [Bun](https://bun.sh/): The Node.js alternative that Elysia is built on. - [Speakeasy CLI](https://www.speakeasy.com/docs/speakeasy-cli/getting-started): The tool you'll use to generate an SDK from the OpenAPI document. ## Adding the Swagger plugin to an Elysia project The Elysia Swagger plugin automatically generates an API documentation page for your server. First, install the Swagger plugin: ```bash filename="Terminal" bun add @elysiajs/swagger ``` Import the plugin, then register it by passing in an instance of `swagger` to the `use()` method and chaining the `use()` method to the `Elysia` instance: ```typescript filename="index.ts" // !mark import { swagger } from "@elysiajs/swagger"; import { Elysia } from "elysia"; import { users } from "./controllers/users"; const app = new Elysia() .onError(({ error, code }) => { console.log({ code }); if (code === "NOT_FOUND") return "Not Found :("; if (code === "VALIDATION") return "Invalid user"; console.error(error); }) .use(users) // !mark .use(swagger()) .listen(3000); ``` In Elysia, a [plugin](https://elysiajs.com/essential/plugin) is a reusable component. In fact, everything in Elysia is a component, including Elysia instances, plugins, routers, stores, and more. Components split apps into small pieces, making it easier to add, remove, or modify app features. It's important that we use [method chaining](https://elysiajs.com/key-concept.html#method-chaining) for type inference in our Elysia code. In the above code block, we use the `onError` lifecycle method to catch any error that's thrown on the server. Run the Bun development server with `bun run dev` and open `http://localhost:3000/swagger` to see the Scalar UI with five API endpoints: ![Scalar UI](/assets/openapi/elysia/scalar.png) The API routes are listed in the navigation pane on the left. Click **`/users/` GET** to navigate to the section for the `/users/` GET request API endpoint: ![GET request API endpoint](/assets/openapi/elysia/scalar-get.png) Each section shows information about an API endpoint, such as its path parameters, body, and responses. The code block on the right shows an example curl request. Click the **Shell cURL** dropdown menu to change the language or library used in the example request: ![Change example request dropdown menu](/assets/openapi/elysia/scalar-change-example-request.png) Click the **Test Request** button to open an API client that lets you test your API endpoints. Then, click **Send** to test the request: ![Scalar API client](/assets/openapi/elysia/scalar-api-client.png) In the response, you should get an array containing one user. The user data in the Elysia server is stored temporarily in a singleton `Users` class : ```typescript filename="users.ts" class Users { constructor( public data: UserInfo[] = [ { id: "1", name: "Alice", age: 20 } ] ) {} ``` This class is added to the `Elysia` instance using the [`decorate`](https://elysiajs.com/essential/handler.html#decorate) method: ```typescript filename="users.ts" mark=2 export const users = new Elysia({ prefix: "/users" }).decorate( "users", new Users(), ); ``` This adds the `Users` class to the [context](https://elysiajs.com/essential/handler.html#context) that contains information for each request. You can access the context in route handlers. ## Viewing the OpenAPI document and modifying its root object Open `http://localhost:3000/swagger/json` to view the OpenAPI document in JSON format: ```json { "openapi": "3.0.3", "info": { "title": "Elysia Documentation", "description": "Development documentation", "version": "0.0.0" }, "paths": { "/users/": { "get": { "operationId": "getUsers", "responses": { "200": { } } }, "post": { // ... ``` Add the following TypeScript configuration option to your `tsconfig.json` file to enable JSON file imports: ```json filename="tsconfig.json" "resolveJsonModule": true, ``` Add the following configuration object to the `swagger` plugin: ```typescript filename="index.ts" import { swagger } from "@elysiajs/swagger"; import { Elysia } from "elysia"; // !mark import packageJson from "../package.json"; import { users } from "./controllers/users"; const app = new Elysia() .onError(({ error, code }) => { if (code === "NOT_FOUND") return "Not Found :("; if (code === "VALIDATION") return "Invalid user"; console.error(error); }) .use(users) .use( // !mark(2:12) swagger({ documentation: { info: { title: "Users app documentation", version: packageJson.version, }, externalDocs: { description: "Find out more about the Users API", url: "www.example.com", }, }, }), ) .listen(3000); ``` This configures the [root document object](https://swagger.io/specification/v3/#openapi-object) of the OpenAPI document. The `info` object is a required property used to add metadata about the API. The `externalDocs` object lets you extend your documentation by referencing an external resource. Note that the API operation for each path has an `operationId` value that's named by combining the HTTP request type and the name of the path. The value can be modified using the [`detail`](https://elysiajs.com/plugins/openapi.html#detail) field in a route; however, we won't modify it in this guide, as Elysia produces consistently named, human-readable `operationId` values. The `operationId` is the identifier for an operation. It is case sensitive and must be unique within the document. The Speakeasy SDK, which we'll use later in this guide, uses it during code generation to name the method it generates for the operation. ## OpenAPI Specification versions supported by Elysia and Speakeasy Speakeasy currently supports the OpenAPI Specification versions 3.0.x and 3.1.x and recommends you use version 3.1, as it's fully compatible with [JSON Schema](https://json-schema.org/), which gives you access to a large ecosystem of tools and libraries. However, we use OpenAPI Specification version 3.0.3 in this guide, as it is the version Elysia supports. To check which version you are using, open `http://localhost:3000/swagger/json` and see the OpenAPI Specification version in the root document object. ## Adding example data to a data model and the POST request body The API routes in the Scalar UI don't have example values for the path parameters, requests, or responses. The `user` model doesn't have example values either. It's important to add examples to make your API more user-friendly. Let's start by adding example values to the `userInfo` model: ```typescript filename="users.ts" const userInfo = t.Object( { id: t.String({ example: "1", }), name: t.String({ example: "Alice", }), age: t.Number({ example: 20, }), }, { title: "User", description: "User object", example: { id: "1", name: "Alice", age: 20, }, }, ); ``` The Elysia schema builder, `t`, gives compile-time and runtime type safety. It also registers the model as a reusable OpenAPI [Components Object](https://swagger.io/specification/v3/#components-object) schema, which you can see at the bottom of your OpenAPI document: ```json "components": { "schemas": { "user": { "example": { "id": "1", "name": "Alice", "age": 20 }, "type": "object", "properties": { "id": { "example": "1", "type": "string" }, "name": { "example": "Alice", "type": "string" }, "age": { "example": 20, "type": "number" } }, "required": [ "id", "name", "age" ] } } } ``` You'll also see the example values in the **`user`** model in the Scalar UI: ![Example values in user model](/assets/openapi/elysia/scalar-user-model-examples.png) The `userInfo` schema is used in the `post()` and `patch()` routes. Elysia HTTP request methods accept three arguments: the path, the function used to respond to the client, and the hook used to define extra metadata. Add an example to the `body` in the hook object of the `post()` route: ```typescript filename="users.ts" mark=4:9 body: t.Omit( userInfo, ['id'], { example: { name: "Alice", age: 20 } } ), ``` If you look at your OpenAPI document now, you'll see that the `content` of the POST request body has three possible types: `application/json`, `multipart/form-data`, or `text/plain`. To limit it to `application/json`, set the `type` in the hook object of the `post()` route: ```typescript filename="users.ts" mark=14 .post('/',({ users, body: user }) => users.add(user), { body: t.Omit( userInfo, ['id'], { example: { name: "Alice", age: 20 } } ), type: 'json', ``` ## Adding extra information to a route using the detail field The [`detail`](https://elysiajs.com/plugins/openapi.html#detail) field is used to define a route for the OpenAPI document. It extends the [OpenAPI Operation Object](https://swagger.io/specification#operation-object), which describes an API operation within a path. Add the following `detail` field to the hook object of the `post()` route: ```typescript filename="users.ts" mark=2:5 type: 'json', detail: { summary: 'Create user', description: 'Add user to the database', }, ``` Add the following `responses` property to the `detail` object: ```typescript filename="users.ts" responses: { 200: { description: 'The created users assigned id', content: { 'application/json': { schema: { $ref: '#/components/schemas/id', }, examples: { "Created user": { value: { id: "1", name: "Alice", age: 20 } } } } }, }, }, ``` The [Responses Object](https://swagger.io/specification/#responses-object) is used to list the possible responses returned from the POST request. There is one possible response listed - a successful response. This response has a [`schema`](https://swagger.io/specification/#schema-object) that defines the content of the response. The `id` schema is referenced using [`$ref`](https://swagger.io/specification/#reference-object), the reference identifier that specifies the URI location of the value being referenced. Let's define this `id` model. Add the following `idObject` model to the `users.ts` file, below the `userInfo` model: ```typescript filename="users.ts" const idObject = t.Object( { id: t.String({ example: "1", }), }, { title: "ID object", description: "ID object", example: { id: "1", }, }, ); ``` Create a [reference model](https://elysiajs.com/tutorial/features/openapi/#reference-model) for the model: ```typescript filename="users.ts" mark=5 export const users = new Elysia({ prefix: "/users" }) .decorate("users", new Users()) .model({ user: userInfo, id: idObject, }); ``` A reference model lets us reuse a model by referencing its name. It's also good practice to add possible error responses. Add the following `500` response to the `responses` property: ```typescript filename="users.ts" 500: { description: 'Server error', content: { 'application/json': { schema: { $ref: '#/components/schemas/errorResponse' }, examples: { "Server error": { value: { message: 'There was an error', status: 500 } } } } } } ``` Add the definition for the `errorResponse` model below the `idObject` model: ```typescript filename="users.ts" const errorResponse = t.Object( { status: t.Number({ example: 404, }), message: t.String({ example: "User not found :(", }), }, { title: "Error response", description: "Error response object", example: { status: 404, message: "User not found :(", }, }, ); ``` Create a reference model for the `errorResponse` model: ```typescript filename="users.ts" mark=6 export const users = new Elysia({ prefix: "/users" }) .decorate("users", new Users()) .model({ user: userInfo, id: idObject, errorResponse: errorResponse, }); ``` You'll now see example responses for the **Create user** POST route in Scalar: ![Example POST request responses](/assets/openapi/elysia/scalar-example-responses.png) ## Adding OpenAPI tags to routes We recommend adding tags to all your Elysia routes. This allows you to group the routes according to tag in the generated SDK code and documentation. ### Adding OpenAPI tags to routes in Elysia To add OpenAPI tags to a route, use the `tags` property to pass in an array of tags in the hook object of the `post()` route: ```typescript filename="users.ts" tags: ["Users"]; ``` ### Adding tags to the root OpenAPI document object and adding metadata to tags Add the following `tags` array to the configuration object of the `swagger` plugin: ```typescript filename="index.ts" mark=13:20 .use( swagger( { documentation: { info: { title: 'Users app documentation', version: packageJson.version, }, externalDocs: { description: 'Find out more about the Users API', url: 'www.example.com', }, tags: [{ name: 'Users', description: 'Users operations', externalDocs: { description: 'Find more info here', url: 'https://example.com', }, }], } }) ) ``` This adds a `tags` array to the root OpenAPI document object. In the above code, we add metadata to the tag by passing in a [Tag Object](https://swagger.io/specification/#tag-object) (instead of a string) to the tag array item. After adding tags to your routes, you'll see that they are organized by tags in Scalar: ![Routes grouped by tag](/assets/openapi/elysia/scalar-grouping-by-tag.png) ## Adding example data, extra information, and tags to the other API routes Let's improve the other API route operations like we improved the **Create user** route. Replace the **Get all users** route with the following lines of code: ```typescript filename="users.ts" mark=2:58 .get('/', ({ users }) => users.data, { detail: { summary: 'Get all users', description: 'Get all users from the database', responses: { 200: { description: 'The array of users', content: { 'application/json': { schema: { type: 'array', items: { $ref: '#/components/schemas/user' }, }, examples: { basic: { value: [ { id: "1", name: "Alice", age: 20 }, { id: "2", name: "Bob", age: 25 } ] } } } }, }, 500: { description: 'Server error', content: { 'application/json': { schema: { $ref: '#/components/schemas/errorResponse' }, examples: { "Server error": { value: { message: 'There was an error', status: 500 } } } } } } }, tags: ['Users'] }, } ) ``` Replace the **Get user** route with the following lines of code: ```typescript filename="users.ts" mark=5:53 .get('/:id',({ users, params: { id }, error }) => { return users.data.find(user => user.id === id) ?? error(404, 'User not found :(') }, { params: 'id', detail: { summary: 'Get user', description: 'Get user by id from the database', responses: { 200: { description: 'The user object', content: { 'application/json': { schema: { $ref: '#/components/schemas/user', }, examples: { basic: { value: { id: "1", name: "Alice", age: 20 } } } } }, }, 404: { description: 'User not found', content: { 'application/json': { schema: { $ref: '#/components/schemas/errorResponse' }, examples: { "User not found": { value: { message: 'User not found :(', status: 404 } } } } } } }, tags: ['Users'] }, }, ) ``` Replace the **Delete user** route with the following lines of code: ```typescript filename="users.ts" mark=5:49 .delete('/:id', ({ users, params: { id }, error }) => { return users.remove(id) ?? error(422, 'Invalid user') }, { params: 'id', detail: { summary: 'Delete user', description: 'Delete user by id from the database', responses: { 200: { description: 'Deleting user was successful', content: { 'application/json': { schema: { $ref: '#/components/schemas/successResponse' }, examples: { success: { value: { success: true } } } } }, }, 422: { description: 'Invalid user', content: { 'application/json': { schema: { $ref: '#/components/schemas/errorResponse' }, examples: { "Invalid user": { value: { message: 'Invalid user', status: 422 } } } } } } }, tags: ['Users'] }, } ) ``` Add the definition for the `successResponse` model below the `errorResponse` model: ```typescript filename="users.ts" const successResponse = t.Object( { success: t.Boolean({ example: true, }), }, { title: "Success response", description: "Success response object", example: { success: true, }, }, ); ``` Create a reference model for the `successResponse` model: ```typescript filename="users.ts" mark=7 export const users = new Elysia({ prefix: "/users" }) .decorate("users", new Users()) .model({ user: userInfo, id: idObject, errorResponse: errorResponse, successResponse: successResponse, }); ``` Replace the **Update user** route with the following lines of code: ```typescript filename="users.ts" mark=7:63 .patch( '/:id',({ users, params: { id }, body: user, error }) => { return users.update(id, user) ?? error(422, 'Invalid user') }, { params: 'id', body: t.Partial( t.Omit( userInfo, ['id'], { example: { age: 21 } } ), ), type: 'json', detail: { summary: 'Update user', description: 'Update user by id from the database', responses: { 200: { description: 'Update was successful', content: { 'application/json': { schema: { $ref: '#/components/schemas/successResponse' }, examples: { Success: { value: { success: true } } } } }, }, 422: { description: 'Invalid user', content: { 'application/json': { schema: { $ref: '#/components/schemas/errorResponse' }, examples: { "Invalid user": { value: { message: 'Invalid user', status: 422 } } } } } } }, tags: ['Users'] }, } ) ``` ## Adding a list of servers to the Elysia OpenAPI document When validating an OpenAPI document, [Speakeasy expects a list of servers](https://www.speakeasy.com/docs/best-practices#openapi-best-practices) at the root of the document. Add a server by adding a `servers` property to the configuration object of the `swagger` plugin: ```typescript filename="index.ts" mark=13:18 .use( swagger( { documentation: { info: { title: 'Users app documentation', version: packageJson.version, }, externalDocs: { description: 'Find out more about the Users API', url: 'www.example.com', }, servers: [ { url: 'http://localhost:3000/', description: 'Development server', }, ], ``` You can add multiple `servers` to define different environments or versions. This is useful for separating production and testing environments. ## Adding retries to your SDK with `x-speakeasy-retries` [OpenAPI document extensions](https://www.speakeasy.com/openapi/extensions) allow us to add vendor-specific functionality to an OpenAPI document. - Extension fields must be prefixed with `x-`. - Speakeasy uses extensions that start with `x-speakeasy-`. Speakeasy gives you fine-tuned control over the Speakeasy SDK via its [range of Speakeasy extensions](https://www.speakeasy.com/docs/speakeasy-extensions), which you can use to modify retries, pagination, error handling, and other advanced SDK features. Let's add a Speakeasy extension that adds retries to requests from Speakeasy SDKs by adding a top-level `x-speakeasy-retries` schema to the OpenAPI document. We can also override the retry strategy per operation. ### Adding global retries Apply the Speakeasy retries extension globally by adding the following `'x-speakeasy-retries'` property to the configuration object of the `swagger` plugin: ```typescript filename="users.ts" mark=7:17 servers: [ { url: 'http://localhost:3000/', description: 'Development server', }, ], 'x-speakeasy-retries': { strategy: 'backoff', backoff: { initialInterval: 500, maxInterval: 60000, maxElapsedTime: 3600000, exponent: 1.5, }, statusCodes: ['5XX'], retryConnectionErrors: true, }, ``` ### Adding retries per method You can create a unique retry strategy for a single route by adding a `'x-speakeasy-retries'` property to the route's hook object: ```typescript filename="users.ts" mark=1:11 'x-speakeasy-retries': { strategy: 'backoff', backoff: { initialInterval: 300, maxInterval: 40000, maxElapsedTime: 3000000, exponent: 1.2, }, statusCodes: ['5XX'], retryConnectionErrors: true, }, tags: ['Users'] }, } ) .delete('/:id', ({ users, params: { id }, error }) => { ``` ## Creating an SDK based on your OpenAPI document Before creating an SDK, we need to save the Elysia Swagger plugin-generated OpenAPI document to a file. OpenAPI files are written as JSON or YAML; we'll save it as a YAML file, as it's easier to read. ### Saving the OpenAPI document to a YAML file using a Bun script Let's create a script that uses the [JS-YAML](https://github.com/nodeca/js-yaml) library to convert the JSON OpenAPI document to a YAML string. Install the library and its types: ```bash filename="Terminal" bun add js-yaml @types/js-yaml ``` Create a script called `generateOpenAPIYamlFile.ts` in the `src` folder and add the following lines of code to it: ```typescript filename="generateOpenAPIYamlFile.ts" import * as yaml from "js-yaml"; async function generateOpenAPIYaml() { try { const response = await fetch("http://localhost:3000/swagger/json"); const openAPIObject = await response.json(); // Convert to YAML const yamlString = yaml.dump(openAPIObject); // Save the YAML string to a file await Bun.write("openapi.yaml", yamlString); console.log("OpenAPI document saved to openapi.yaml"); } catch (error) { console.error("Error generating OpenAPI document:", error); } } generateOpenAPIYaml(); ``` This script fetches the JSON OpenAPI document from the Scalar OpenAPI document endpoint, converts it to a YAML string, and then saves it as a file. Add the following script to your `package.json` file: ```bash filename="Terminal" "generate:openapi": "bun run src/generateOpenAPIYamlFile.ts" ``` Run the development server and then run the `generate:openapi` script using the following command: ```bash filename="Terminal" bun run generate:openapi ``` This generates an `openapi.yaml` file in your root folder. ### Linting the OpenAPI document with Speakeasy The Speakeasy CLI has an OpenAPI [linting](https://www.speakeasy.com/docs/linting) command that checks the OpenAPI document for errors and style issues. Run the linting command: ```bash filename="Terminal" speakeasy lint openapi --schema ./openapi.yaml ``` A lint report will be displayed in the terminal, showing errors, warnings, and hints: ![Speakeasy lint report](/assets/openapi/elysia/speakeasy-lint-report.png) The Speakeasy linter uses the [`speakeasy-recommended`](https://www.speakeasy.com/docs/linting/linting#speakeasy-recommended) ruleset by default, but you can [configure](https://www.speakeasy.com/docs/linting#configuration) a custom ruleset. ### Creating an SDK from the Speakeasy CLI We'll use the [`quickstart`](https://www.speakeasy.com/docs/speakeasy-cli/quickstart) command for a guided SDK setup. Run the command using the Speakeasy CLI: ```bash filename="Terminal" speakeasy quickstart ``` Following the prompts, provide the OpenAPI document location, name the SDK `SDK`, and select `TypeScript` as the SDK language. In the terminal, you'll see the steps taken by Speakeasy to generate the SDK. ```bash filename="Terminal" │ Workflow - success │ └─Target: sdk - success │ └─Source: SDK -OAS - success │ └─Validating Document - success │ └─Diagnosing OpenAPI - success │ └─Tracking OpenAPI Changes - success │ └─Snapshotting OpenAPI Revision - success │ └─Storing OpenAPI Revision - success │ └─Validating gen.yaml - success │ └─Generating Typescript SDK - success │ └─Setup Environment - success │ └─Load and Validate Document - success │ └─Generate SDK - success │ └─Compile SDK - success ``` Speakeasy [validates](https://www.speakeasy.com/docs/concepts#validation) the OpenAPI document to check that it's ready for code generation. Validation issues will be printed in the terminal. The generated SDK is saved as a folder in your project. If you get ESLint styling errors, run the `speakeasy quickstart` command from outside your project. ## Adding SDK generation to your GitHub Actions The Speakeasy [`sdk-generation-action`](https://github.com/speakeasy-api/sdk-generation-action) repository provides workflows that integrate the Speakeasy CLI into CI/CD pipelines and automatically regenerate client SDKs when the reference OpenAPI document changes. You can configure Speakeasy to push a new branch to your SDK repositories automatically when the OpenAPI document changes, allowing your engineers to review and merge the SDK changes. The Speakeasy [workflow matrix](https://www.speakeasy.com/docs/workflow-reference/generation-reference) provides an overview of how to set up automatic SDK generation. ## Using your SDK Once you've generated your SDK, you can [publish](https://www.speakeasy.com/docs/publish-sdks) it for use. TypeScript SDKs are published as npm packages. A quick, non-production-ready way to see your SDK in action is to copy your SDK folder to a frontend TypeScript project and use it there. For example, you can create a Vite project that uses TypeScript: ```bash filename="Terminal" npm create vite@latest ``` Then, copy the SDK folder from your Elysia app to the `src` directory of your TypeScript Vite project and delete the SDK folder in your Elysia project. In the SDK `README.md` file, you'll find the documentation for your Speakeasy SDK. Note that the SDK is not ready for production use. To get it production-ready, follow the steps outlined in your Speakeasy workspace. The SDK includes Zod as a bundled dependency, as can be seen in the `sdk-typescript/package.json` file. Replace the code in the `src/main.ts` file with the following example code taken from the `sdk-typescript/docs/sdks/users/README.md` file: ```typescript filename="main.ts" import { SDK } from "./sdk-typescript/src/"; // Adjust the path as necessary e.g. if your generated SDK has a different name const sdk = new SDK(); async function run() { const result = await sdk.users.getUsers(); // Handle the result console.log({ result }); } run(); ``` Make sure the Elysia server is running, then run the Vite dev server: ```bash filename="Terminal" npm run dev ``` Now, you need to enable CORS in your Elysia dev server. First, install the CORS plugin: ```bash filename="Terminal" bun add @elysiajs/cors ``` Import the CORS plugin, then register it by passing the plugin into the `use()` method and chaining the `use()` method to the `Elysia` instance: ```typescript filename="index.ts" // !mark(1,8:12) import { cors } from "@elysiajs/cors"; const app = new Elysia() .onError(({ error, code }) => { if (code === "NOT_FOUND") return "Not Found :("; console.error(error); }) .use( cors({ origin: "http://localhost:5173", }), ); ``` Open `http://localhost:5173` in your browser, then open your browser dev tools. You should see the following logged in the dev tools console: ``` { "result": [ { "id": "1", "name": "Alice", "age": 20 } ] } ``` The SDK functions are type safe and include TypeScript autocompletion for arguments and outputs. Consider the following example scenario: ```typescript filename="main.ts" const userOne = result[0].email; ``` When you try to access a property that doesn't exist, as in the code block above, you get a TypeScript error: ``` Property 'email' does not exist on type 'User'. ``` ## Further reading This guide covered the basics of generating an OpenAPI document using Elysia. Here are some resources to help you learn more about OpenAPI, the Elysia Swagger plugin, and Speakeasy: - [Elysia Swagger plugin](https://elysiajs.com/plugins/openapi): Learn more about using Elysia to generate OpenAPI documents. Elysia has first-class support for OpenAPI and follows the OpenAPI Specification by default. - [Speakeasy documentation](https://www.speakeasy.com/docs): Speakeasy has extensive documentation covering how to generate SDKs from OpenAPI documents, customize SDKs, and more. - [Speakeasy OpenAPI reference](https://www.speakeasy.com/openapi): Review a detailed reference on the OpenAPI Specification. # How to generate an OpenAPI document with FastAPI Source: https://speakeasy.com/openapi/frameworks/fastapi import { Callout, YouTube } from "@/mdx/components";
Many developers start their API development with FastAPI, and with good reason. FastAPI has rapidly gained traction in the Python community for its excellent performance, intuitive design, and flexibility. It enables developers to craft API solutions that not only run fast but also meet their users' unique needs. FastAPI is great for building your core API, but you'll want to layer on SDKs and docs to provide your users with easy integration. For that, you'll want an OpenAPI document. The good news is that FastAPI provides you with an OpenAPI document out of the box. The less good news is that you'll need some tweaking to get the OpenAPI document to a level where it becomes usable with other tooling. This article will show you how to improve the default OpenAPI document generation to make the most of the generated schema. ## Generating an OpenAPI document with FastAPI Understanding how FastAPI generates OpenAPI documents can help you make more informed decisions when you customize your FastAPI setup. The process is fairly straightforward: FastAPI builds the OpenAPI document based on the routes and models you've defined in your application. For every route in your FastAPI application, FastAPI adds an operation to the OpenAPI document. For every model used in these routes, FastAPI adds a schema definition. The request and response bodies, parameters, and headers all draw from these schema definitions. While this process works well out of the box, FastAPI also offers several customization options that can change the generated OpenAPI document. We'll cover some of these options in the following sections. ## Our FastAPI example app: APItizing Burgers Let's get this out of the way: The name came in a daydream shortly before lunchtime. To guide us through this journey, we'll use a simple example FastAPI application: the "APItizing Burgers" burger shop API. This API includes two models, `Burger` and `Order`, and provides basic CRUD operations for managing burgers and orders at our hypothetical burger shop. Additionally, we have a webhook defined for order status events. We'll look at how we optimized this FastAPI application and refined our models and routes so that the generated OpenAPI document is intuitive and easy to use. We will also explore how we can use this schema to generate SDKs using Speakeasy. The source code for our example API is available in the [framework-fastapi](https://github.com/speakeasy-api/examples/tree/main/framework-fastapi) folder of the Speakeasy Examples repository. The `framework-fastapi` directory consists of two directories: `app` and `sdk`. The `app` directory contains the FastAPI server definition: `app/main.py`. This is where we'll look at what we customized. The `sdk` directory and the two OpenAPI documents, `openapi.yaml` and `openapi.json`, are generated by running `gen.sh` in the root of the project. Join us as we dive into FastAPI customization and discover how these tweaks can streamline your SDK generation process. When using Pydantic to define models, a known issue is that the serialization of `datetime` objects is not timezone-aware. This will cause a mismatch with the OpenAPI format `date-time`, which requires RFC 3339 date-time strings with timezones included. Consider using [`AwareDatetime`](https://docs.pydantic.dev/2.5/api/types/#pydantic.types.AwareDatetime) fields in Pydantic models to enable the appropriate [validation](https://docs.pydantic.dev/latest/errors/validation_errors/#timezone_aware) and ensure your SDK behavior matches the response definition from your server. ## Basic FastAPI setup Let's get started with the basics – some things you probably do already. These straightforward examples are trivial but will help you better understand the three steps in the automation pipeline: How FastAPI setup influences OpenAPI documents, which, in turn, influences SDK code. ### Scalar API documentation FastAPI automatically generates API documentation using Swagger UI. This example also has [Scalar](https://scalar.com/) API documentation. Scalar is an alternative to Swagger UI. Its standout feature, as explained in our blog post, [Choosing a docs vendor: Mintlify vs Scalar vs Bump vs ReadMe vs Redocly](https://www.speakeasy.com/blog/choosing-a-docs-vendor#mintlify-seriously-beautiful-docs), is that it can be used as a standalone API client. Scalar is also easy to use and has a modern UI that can be customized to create branded documentation. We use it for our [Speakeasy docs](https://www.speakeasy.com/blog/release-speakeasy-docs#why-we-partnered-with-scalar). We added Scalar to the FastAPI app by installing the [Scalar FastAPI plugin](https://github.com/scalar/scalar/blob/main/integrations/fastapi/README.md). After installing the plugin, we added a "/scalar" route, in `app/main.py`, that returns the Scalar API Reference: ```python from scalar_fastapi import get_scalar_api_reference # ... app = FastAPI() # ... @app.get("/scalar", include_in_schema=False) async def scalar_html(): return get_scalar_api_reference( openapi_url=app.openapi_url, title=app.title + " - Scalar", ) ``` ### Server configuration This may seem obvious, but while first working with FastAPI in development, the generated docs, development server, and API operations all work out of the box without the need to manually specify your server address. However, when generating SDKs, your OpenAPI document needs to list servers. In our `app/main.py`, we added our local server as shown: ```python from fastapi import FastAPI app = FastAPI( servers=[ {"url": "http://127.0.0.1:8000", "description": "Local server"}, ], ) ``` This leads to the following generated output in `openapi.yaml`: ```yaml # The basic server configuration in OpenAPI servers: - description: Local server url: http://127.0.0.1:8000/ # You can add additional servers if needed # - description: Production server # url: https://api.example.com/ ``` ### Application information In our `app/main.py`, if we have the following: ```python from fastapi import FastAPI app = FastAPI( summary="A simple API to manage burgers and orders", description="This API is used to manage burgers and orders in a restaurant", version="0.1.0", title="APItizing Burger API", ) ``` FastAPI generates the following YAML in our `openapi.yaml` file: ```yaml info: description: This API is used to manage burgers and orders in a restaurant summary: A simple API to manage burgers and orders title: APItizing Burger API version: 0.1.0 ``` ### Route customizations With the basics out of the way, let's look at a few more substantial recommendations. ### Typed responses When developers use your generated SDK, they may wish to see what all the possible responses for an API call could be. With FastAPI, you can add additional responses to each route by specifying a response type. In our `app/main.py`, we added this abbreviated code: ```python from fastapi import FastAPI from fastapi.responses import JSONResponse from pydantic import BaseModel, Field class ResponseMessage(BaseModel): """A response message""" message: str = Field(description="The response message") OPENAPI_RESPONSE_BURGER_NOT_FOUND = { "model": ResponseMessage, "description": "Burger not found", } def response_burger_not_found(burger_id: int): """Response for burger not found""" return JSONResponse( status_code=404, content=f"Burger with id {burger_id} does not exist", ) class Burger(BaseModel): id: int name: str description: str = None app = FastAPI() @app.get( "/burger/{burger_id}", response_model=BurgerOutput, responses={404: OPENAPI_RESPONSE_BURGER_NOT_FOUND}, tags=["burger"], ) def read_burger(burger_id: Annotated[int, Path(title="Burger ID")]): """Read a burger""" for burger in burgers_db: if burger.id == burger_id: return burger return response_burger_not_found(burger_id) ``` FastAPI adds a schema for our specific error message to `openapi.yaml`: ```yaml components: schemas: ResponseMessage: description: A response message properties: message: description: The response message title: Message type: string required: - message title: ResponseMessage type: object ``` ### Operation tags As your API develops and grows bigger, you're likely to split it into separate files. FastAPI [provides conveniences](https://fastapi.tiangolo.com/tutorial/bigger-applications/) to help reduce boilerplate and repetition when splitting an API into multiple modules. While this separation may reduce cognitive overhead while you're working in particular sections of the API code, it doesn't mean similar groups are automatically created in your documentation and SDK code. We recommend you add tags to all operations in FastAPI, whether you're building a big application or only have a handful of operations, so that operations can be grouped by tag in generated SDK code and documentation. ```yaml filename="operation-tags.yaml" # Tags help organize operations into logical groups tags: - name: burger - name: order ``` The most straightforward way to add tags is to edit each operation and add a list of tags. This example highlights the tags list: ```python from fastapi import FastAPI app = FastAPI() @app.get( "/burger/{burger_id}", tags=["burger"], ) def read_burger(burger_id: int): return { "burger_id": burger_id, } ``` ### Tag metadata You can add metadata to your tags to further improve the developer experience. FastAPI accepts a parameter called `openapi_tags`, which we can use to add metadata, such as a description and a list of external documentation links. Here's how to add metadata to tags: ```python from fastapi import FastAPI tags_metadata = [ { "name": "burger", "description": "Operations related to burgers", "externalDocs": { "description": "Burger external docs", "url": "https://en.wikipedia.org/wiki/Hamburger", }, }, { "name": "order", "description": "Operations related to orders", }, ] app = FastAPI( openapi_tags=tags_metadata, ) @app.get( "/burger/{burger_id}", tags=["burger"], ) def read_burger(burger_id: int): return { "burger_id": burger_id, } ``` When we add metadata to tags, FastAPI adds a top-level `tags` section to our OpenAPI document: ```yaml tags: - description: Operations related to burgers externalDocs: description: Burger external docs url: https://en.wikipedia.org/wiki/Hamburger name: burger - description: Operations related to orders name: order ``` Each tagged path in our OpenAPI document also gets a list of tags: ```yaml paths: /burger/{burger_id}: get: description: Read a burger operationId: readBurger summary: Read Burger tags: - burger # ... ``` ### Operation ID customization When FastAPI outputs an OpenAPI document, it generates a unique OpenAPI `operationId` for each path. By default, this unique ID is generated by the FastAPI `generate_unique_id` function: ```python def generate_unique_id(route: "APIRoute") -> str: operation_id = route.name + route.path_format operation_id = re.sub(r"\W", "_", operation_id) assert route.methods operation_id = operation_id + "_" + list(route.methods)[0].lower() return operation_id ``` This can often lead to cumbersome and unintuitive names. To improve usability, we have two methods of customizing these generated strings: 1. Using a custom `generate_unique_id_function` 2. Specifying `operation_id` per operation The preferred method is to use a custom function when you generate unique IDs for paths. The example below is an illustrative function that doesn't generate guaranteed-unique IDs and doesn't handle method names without an underscore. However, it demonstrates how you can add a function that generates IDs based on an operation's method name: ```python from fastapi import FastAPI def convert_snake_case_to_camel_case(string: str) -> str: """Convert snake case to camel case""" words = string.split("_") return words[0] + "".join(word.title() for word in words[1:]) def custom_generate_unique_id_function(route: APIRoute) -> str: """Custom function to generate unique id for each endpoint""" return convert_snake_case_to_camel_case(route.name) app = FastAPI( generate_unique_id_function=custom_generate_unique_id_function, ) ``` With FastAPI, you can also specify the `operationId` per operation. For our example, we'll add a new parameter called `operation_id` to the operation decorator: ```python from fastapi import FastAPI app = FastAPI() @app.get( "/burger/{burger_id}", operation_id="readBurger", ) def read_burger(burger_id: int): pass ``` ### Webhooks Starting with OpenAPI version 3.1.0, it is possible to specify webhooks for your application in OpenAPI. Here's how to add a webhook to FastAPI: ```python from fastapi import FastAPI app = FastAPI() class Order(BaseModel): id: int note: str @app.webhooks.post( "order-status-changed", operation_id="webhookOrderStatusChanged", ) def webhook_order_status_changed(body: Order): """ When an order status is changed, this webhook will be triggered. The server will send a `POST` request with the order details to the webhook URL. """ pass ``` FastAPI generates the following top-level `webhooks` section in `openapi.yaml`: ```yaml webhooks: order-status-changed: post: description: "When an order status is changed, this webhook will be triggered. The server will send a `POST` request with the order details to the webhook URL." operationId: webhookOrderStatusChanged requestBody: content: application/json: schema: $ref: "#/components/schemas/Order" required: true responses: "200": content: application/json: schema: {} description: Successful Response "422": content: application/json: schema: $ref: "#/components/schemas/HTTPValidationError" description: Validation Error summary: Webhook Order Status Changed ``` ## Speakeasy integration Now that we have a customized OpenAPI document, we can use Speakeasy to generate SDKs based on it. You can generate an SDK by running the `gen.sh` bash script, which runs the `speakeasy generate` command. Alternatively, you can use the Speakeasy quick start command for a guided SDK setup: ```bash filename="Terminal" speakeasy quickstart ``` Following the prompts, provide the OpenAPI document location, name your SDK, and select the SDK language. In the terminal, you'll see the steps taken by Speakeasy to create the SDK: ``` │ └─Source: APItizing Burgers API - success │ └─Validating Document - success │ └─Diagnosing OpenAPI - success │ └─Tracking OpenAPI Changes - success │ └─Snapshotting OpenAPI Revision - success │ └─Storing OpenAPI Revision - success │ └─Validating gen.yaml - success │ └─Generating Python SDK - success │ └─Setup Environment - success │ └─Load and Validate Document - success │ └─Generate SDK - success │ └─Compile SDK - success │ └─Setup Environment - success │ └─Load and Validate Document - success │ └─Generate SDK - success │ └─Generating Code Samples - success │ └─Snapshotting Code Samples - success │ └─Snapshotting Code Samples - success │ └─Uploading Code Samples - success ``` Speakeasy [validates](/docs/concepts#validation) the OpenAPI document to check that it's ready for code generation. Validation issues will be printed in the terminal and the generated SDK will be saved as a folder in your project. Speakeasy also suggests improvements for your SDK using [Speakeasy Suggest](/docs/prep-openapi/maintenance), which is an AI-powered tool in Speakeasy Studio. You can see suggestions by opening the link to your Speakeasy Studio workspace in the terminal. Let's take a look at how the information we detailed in the OpenAPI document affects how Speakeasy generates SDKs. Adding the local server information leads to the following generated output in the `openapi.yaml` file: ```yaml # This server configuration will be used by Speakeasy when generating SDKs openapi: 3.1.0 info: title: APItizing Burgers API version: 0.1.0 servers: - description: Local server url: http://127.0.0.1:8000/ ``` After Speakeasy generates the SDK, this leads to the following abbreviated code in `sdk/src/openapi/sdkconfiguration.py`: ```python from dataclasses import dataclass SERVERS = [ 'http://127.0.0.1:8000/', # Local server ] """Contains the list of servers available to the SDK""" @dataclass class SDKConfiguration: ... server_url: Optional[str] = "" server_idx: Optional[int] = 0 ... def __post_init__(self): self._hooks = SDKHooks() def get_server_details(self) -> Tuple[str, Dict[str, str]]: if self.server_url is not None and self.server_url: return remove_suffix(self.server_url, "/"), {} if self.server_idx is None: self.server_idx = 0 return SERVERS[self.server_idx], {} ``` You'll find calls to `SDKConfiguration.get_server_details()` when the SDK builds API URLs: ```python from dataclasses import dataclass SERVERS = [ 'http://127.0.0.1:8000/', # Local server ] """Contains the list of servers available to the SDK""" @dataclass class SDKConfiguration: ... server_url: Optional[str] = "" server_idx: Optional[int] = 0 ... def __post_init__(self): self._hooks = SDKHooks() def get_server_details(self) -> Tuple[str, Dict[str, str]]: if self.server_url is not None and self.server_url: return remove_suffix(self.server_url, "/"), {} if self.server_idx is None: self.server_idx = 0 return SERVERS[self.server_idx], {} ``` Speakeasy uses the title, summary, and descriptions we provided earlier to add helpful text to the generated SDK documentation, including comments in the SDK code. For example, in `sdk/src/openapi/sdk.py`: ```python class SDK(BaseSDK): r"""APItizing Burgers API: A simple API to manage burgers and orders This API is used to manage burgers and orders in a restaurant """ ``` Speakeasy adds the version to the `SDKConfiguration` in `sdk/src/openapi/sdkconfiguration.py`. It also uses this version to construct the user agent (`user_agent`), which contains the version of the SDK, the version of the Speakeasy generator build, and the version of the OpenAPI documentation: ```python from dataclasses import dataclass @dataclass class SDKConfiguration: ... openapi_doc_version: str = '0.1.0' user_agent: str = "speakeasy-sdk/python 0.2.0 2.607.0 0.1.0 openapi" ... ``` When users call your API using the generated SDK, the `user_agent` from `SDKConfiguration` is automatically added to the `user-agent` header. The `_build_request_with_client` method in `BaseSDK` constructs the HTTP request and sets the header using `headers[user_agent_header] = self.sdk_configuration.user_agent`: ```python def _build_request_with_client( self, ... user_agent_header, ... ) -> httpx.Request: ... headers["Accept"] = accept_header_value headers[user_agent_header] = self.sdk_configuration.user_agent ... ``` ### Operation ID issues and solutions The unique `operation_id` generated by FastAPI does not translate well into an SDK. We need to customize the unique `operation_id` that FastAPI generates for better readability. For instance, in the operation that returns a burger by `burger_id`, the default unique ID would be `read_burger_burger__burger_id__get`. This makes its way into SDK code, leading to class names such as `ReadBurgerBurgerBurgerIDGetRequest` or function names like `read_burger_burger_burger_id_get`. Here's a usage example after generating an SDK without customizing the `operationId`: ```python import sdk from sdk.models import operations s = sdk.SDK() req = operations.ReadBurgerBurgerBurgerIDGetRequest( burger_id=847252, ) res = s.burger.read_burger_burger_burger_id_get(req) ``` However, after using the custom function `generate_unique_id` we defined previously, the `read_burger` operation gets a friendlier operation ID, `readBurger`, and the usage example becomes easier to read: ```python import sdk from sdk.models import operations s = sdk.SDK() req = operations.ReadBurgerRequest( burger_id=847252, ) res = s.burger.read_burger(req) ``` In addition to the two methods described earlier, there is a third way to customize the `operation_id`. We can add the top-level `x-speakeasy-name-override` [Speakeasy extension](https://www.speakeasy.com/docs/speakeasy-reference/extensions) to our OpenAPI document, allowing Speakeasy to override the generated names when it generates SDK code. To add this extension, follow the Speakeasy guide to [changing method names](/docs/customize-sdks/methods). ```yaml filename="name-override.yaml" # Example of x-speakeasy-name-override extension x-speakeasy-name-override: operations: readBurger: getBurger createBurger: addBurger ``` ### Adding retry functionality Speakeasy can generate SDKs that follow custom rules for retrying failed requests. For instance, if your server fails to return a response within a specified time, you may want your users to retry their request without clobbering your server. To add retries to SDKs generated by Speakeasy, add a top-level `x-speakeasy-retries` schema to your OpenAPI document. You can also override the retry strategy per operation by adding `x-speakeasy-retries` to each operation: ```yaml filename="speakeasy-retries.yaml" # Speakeasy retries can be configured with a top-level extension x-speakeasy-retries: strategy: backoff statusCodes: [5XX] retryConnectionErrors: true ``` To add global retries, we need to customize the schema generated by the FastAPI `get_openapi` function: ```python from fastapi import FastAPI from fastapi.openapi.utils import get_openapi app = FastAPI( summary="A simple API to manage burgers and orders", description="This API is used to manage burgers and orders in a restaurant", version="0.1.0", title="APItizing Burger API", ) def custom_openapi(): if app.openapi_schema: return app.openapi_schema openapi_schema = get_openapi( title=app.title, version=app.version, summary=app.summary, description=app.description, routes=app.routes, ) # Add retries openapi_schema["x-speakeasy-retries"] = { "strategy": "backoff", "backoff": { "initialInterval": 500, "maxInterval": 60000, "maxElapsedTime": 3600000, "exponent": 1.5, }, "statusCodes": [ "5XX", ], "retryConnectionErrors": True, } app.openapi_schema = openapi_schema return app.openapi_schema app.openapi = custom_openapi ``` Keep in mind that you'll need to add this customization _after_ declaring your operation routes. This change adds the following top-level section to `openapi.yaml`: ```yaml x-speakeasy-retries: backoff: exponent: 1.5 initialInterval: 500 maxElapsedTime: 3600000 maxInterval: 60000 retryConnectionErrors: true statusCodes: - 5XX strategy: backoff ``` To add `x-speakeasy-retries` to a single operation, update the operation and add the `openapi_extra` parameter as follows: ```python from fastapi import FastAPI app = FastAPI() @app.get( "/burger/", openapi_extra={ "x-speakeasy-retries": { "strategy": "backoff", "backoff": { "initialInterval": 500, "maxInterval": 60000, "maxElapsedTime": 3600000, "exponent": 1.5, }, "statusCodes": [ "5XX", ], "retryConnectionErrors": True, } }, ) def list_burgers(): return [] ``` ### Authentication and security FastAPI supports several authentication mechanisms that can easily be integrated into your API. The example below demonstrates adding an API key authentication scheme to the `/burger/` endpoint of our API. We use the `APIKeyHeader` dependency to validate the API key passed in the `Authorization` header: ```python from fastapi.security import APIKeyHeader API_KEY = "your-apitizing-api-key" header_scheme = APIKeyHeader( name=API_KEY, auto_error=True, description="API Key for the Burger listing API. API Key should be sent as a header, with the value 'your-apitizing-api-key'", scheme_name="api_key", ) ``` We can pass a `key` parameter to the `list_burgers` function, retrieve the API key from the header, and perform validation: ```python @app.get( "/burger/", response_model=List[BurgerOutput], tags=["burger"], ... ) def list_burgers(key: str = Depends(header_scheme)): """List all burgers""" if key != API_KEY: raise HTTPException(status_code=401, detail="Invalid API Key") return [BurgerOutput(**burger_data.dict()) for burger_data in burgers_db] ``` Now when generating the OpenAPI document, the API key authentication scheme will be included and only required for the listing on the `/burger/` endpoint. ### Handling form data Form data is a common way to receive information from clients, particularly in web applications. FastAPI provides robust support for form data through its `Form` class, and it correctly documents these form fields in the OpenAPI schema. When working with form data in FastAPI, you need to use the `Form` class from the `fastapi` module. This ensures that FastAPI correctly adds the appropriate schema information to your OpenAPI document. ```python from fastapi import FastAPI, Form from typing import Annotated app = FastAPI() @app.post("/burger/create/") async def create_burger_form( name: Annotated[str, Form()], description: Annotated[str, Form()], price: Annotated[float, Form()] ): """Create a new burger using form data""" return { "name": name, "description": description, "price": price } ``` In the OpenAPI document, FastAPI correctly identifies this endpoint as accepting form data: ```yaml paths: /burger/create/: post: summary: Create Burger Form operationId: createBurgerForm requestBody: content: application/x-www-form-urlencoded: schema: type: object properties: name: type: string description: type: string price: type: number required: - name - description - price responses: "200": description: Successful Response content: application/json: schema: {} tags: - burger ``` When Speakeasy generates an SDK from this OpenAPI document, it will correctly handle form data submissions. The generated SDK will provide a clean interface for submitting form data: ```python import sdk from sdk.models import operations s = sdk.SDK() req = operations.CreateBurgerFormRequest( name="Classic Burger", description="Our signature burger with special sauce", price=8.99 ) res = s.burger.create_burger_form(req) ``` #### File uploads FastAPI also supports file uploads, both as individual files or as multiple files. The OpenAPI schema will correctly document these endpoints as accepting multipart form data. ```python from fastapi import FastAPI, File, UploadFile from typing import Annotated app = FastAPI() @app.post("/burger/image/") async def upload_burger_image( burger_id: Annotated[int, Form()], image: Annotated[UploadFile, File()] ): """Upload an image for a burger""" contents = await image.read() # Process file contents... return { "burger_id": burger_id, "filename": image.filename, "content_type": image.content_type, "size": len(contents) } ``` This endpoint will be documented in the OpenAPI schema with `multipart/form-data` content type, allowing Speakeasy to generate appropriate SDK code for handling file uploads. #### Advanced form validation For form data that needs validation beyond simple type checking, you can combine Pydantic models with the `Form` class. First, define your model with the validation rules: ```python from pydantic import BaseModel, Field class BurgerFormData(BaseModel): name: str = Field(..., min_length=3, max_length=50) description: str = Field(..., min_length=10, max_length=200) price: float = Field(..., gt=0, le=100) ``` Then use the model fields with form data: ```python @app.post("/burger/create/validated/") async def create_burger_validated( name: Annotated[str, Form()], description: Annotated[str, Form()], price: Annotated[float, Form()] ): burger_data = BurgerFormData( name=name, description=description, price=price ) return burger_data ``` FastAPI will generate an OpenAPI document that includes these validation constraints, allowing SDK users to understand the requirements before submitting the form. ## Summary In this post, we've explored how you can set up a FastAPI-based SDK generation pipeline without hand-editing or updating OpenAPI documents. By using existing FastAPI methods for extending and customizing OpenAPI documents, you can improve the usability of your generated client SDKs. # How to generate an OpenAPI/Swagger spec with Fastify Source: https://speakeasy.com/openapi/frameworks/fastify import { Callout } from "@/mdx/components"; In this tutorial, we'll show you how to generate an OpenAPI specification using [Fastify](https://fastify.dev/). We will also show how you can use Speakeasy to generate client SDKs for your API based on the specification. If you want to follow along with the example code in this tutorial, you can clone the [Speakeasy Fastify example repo](https://github.com/speakeasy-api/guide-fastify-example). Here's what we'll cover: 1. How to add `@fastify/swagger` to a Fastify project. 2. Generate an OpenAPI specification using the Fastify CLI. 3. Improve the generated OpenAPI specification for better downstream SDK generation. 4. Use the Speakeasy CLI to generate a client SDK based on our generated OpenAPI specification. 5. Use the Speakeasy OpenAPI extensions to improve generated SDKs. 6. How to automate this process as part of a CI/CD pipeline. Your Fastify project might not be as simple as our example app, but the steps below should translate well to any Fastify project. We'll also look at how to gradually add routes to OpenAPI so that you have the option to ship an SDK that improves API coverage over time. ## The SDK Generation Pipeline Fastify ships with the [`@fastify/swagger`](https://github.com/fastify/fastify-swagger) plugin, which provides convenient shortcuts for generating good OpenAPI specifications. We'll start this tutorial by registering `@fastify/swagger` in a Fastify project to generate a spec. The quality of your OpenAPI specification will ultimately determine the quality of generated SDKs and documentation, so we'll dive into ways you can improve the generated specification. With our new and improved OpenAPI specification in hand, we'll take a look at how to generate SDKs using Speakeasy. Finally, we'll add this process to a CI/CD pipeline so that Speakeasy automatically generates fresh SDKs whenever your Fastify API changes in the future. ## Requirements This guide assumes that you have an existing Fastify app or you'll clone our example application, and that you have a basic familiarity with Fastify. You'll need [Node.js installed](https://nodejs.org/en/download) (we used Node v20.5.1), and you'll need to install the [Fastify CLI](https://github.com/fastify/fastify-cli/). Once you have Node.js, you can install the Fastify CLI by running the following in the terminal: ```bash npm install fastify-cli --global ``` Make sure `fastify` is in your `$PATH`: ```bash fastify version ``` If you can't run `fastify` using the steps above, you can use `npx` to run `fastify-cli` by replacing `fastify` with `fastify-cli` in our code samples. For example: ```bash # fastify version npx fastify-cli version ``` Install the [Speakeasy CLI](/docs/speakeasy-cli/getting-started) to generate the SDK once you have generated your OpenAPI spec. ## How To Add "@fastify/swagger" to a Fastify Project In your Fastify project folder, run the following in the terminal to install `@fastify/swagger`: ```bash npm install --save @fastify/swagger ``` To register `@fastify/swagger` in our Fastify app, we added a new plugin. Here's the simplified plugin we added as `plugins/openapi.js`: ```javascript filename="plugins/openapi.js" import swagger from "@fastify/swagger"; import fp from "fastify-plugin"; export default fp(async (fastify) => { fastify.register(swagger); }); ``` Without any further configuration, you can generate an OpenAPI specification by running the Fastify CLI: ```bash fastify generate-swagger app.js ``` This should print a basic OpenAPI spec in JSON format. If you find YAML more readable than JSON, you can add `--yaml=true` to your `fastify` commands: ```bash fastify generate-swagger --yaml=true app.js ``` The option to output YAML is [brand new](https://github.com/fastify/fastify-cli/pull/662) and, while merged, hasn't made it to a release when we wrote this tutorial. ## Supported OpenAPI Versions in Fastify and Speakeasy Fastify can generate OpenAPI specifications in [OpenAPI version 2.0](https://swagger.io/specification/v2/) (formerly known as _Swagger_) or [OpenAPI version 3.0.3](https://swagger.io/specification/v3/). Speakeasy supports OpenAPI 3.x. We need to configure Fastify to ensure we output an OpenAPI spec that conforms to OpenAPI 3.0.3. ### How To Generate a Specification in OpenAPI Version 3.0.3 Using Fastify In Fastify, the version of the generated OpenAPI specification is determined by the Fastify options object. To use OpenAPI 3.0.3, the options object should contain an object with the key `openapi` at its root. Continuing our example above, we'll add an options object when we register `@fastify/swagger` in `plugins/openapi.js`: ```javascript filename="plugins/openapi.js" mark=5:7 import swagger from "@fastify/swagger"; import fp from "fastify-plugin"; export default fp(async (fastify) => { fastify.register(swagger, { openapi: {}, }); }); ``` To verify that we now have an OpenAPI 3.0.3 spec, run: ```bash fastify generate-swagger app.js ``` The output should start with the following JSON: ```json mark=2 { "openapi": "3.0.3" //... } ``` ## How To Add OpenAPI "info" in Fastify Without customization, `@fastify/swagger` generates the following `info` object for our API: ```json { //... "info": { "version": "8.10.1", "title": "@fastify/swagger" } } ``` We can customize this object by updating our options object in `plugins/openapi.js`: ```javascript filename="plugins/openapi.js" mark=7:21 import swagger from "@fastify/swagger"; import fp from "fastify-plugin"; export default fp(async (fastify) => { fastify.register(swagger, { openapi: { info: { title: "Speakeasy Bar API", description: "This is a sample API for Speakeasy Bar.", termsOfService: "http://example.com/terms/", contact: { name: "Speakeasy Bar Support", url: "http://www.example.com/support", email: "support@example.com", }, license: { name: "Apache 2.0", url: "https://www.apache.org/licenses/LICENSE-2.0.html", }, version: "1.0.1", }, }, }); }); ``` Fastify copies this `info` object verbatim, which results in the following `info` object in our JSON: ```json { "info": { "title": "Speakeasy Bar API", "description": "This is a sample API for Speakeasy Bar.", "termsOfService": "http://example.com/terms/", "contact": { "name": "Speakeasy Bar Support", "url": "http://www.example.com/support", "email": "support@example.com" }, "license": { "name": "Apache 2.0", "url": "https://www.apache.org/licenses/LICENSE-2.0.html" }, "version": "1.0.1" } //... } ``` Another common pattern we've seen, included here for completeness, is to reuse information from the project's `package.json` when generating OpenAPI specs. This pattern takes [DRY](https://en.wikipedia.org/wiki/Don%27t_repeat_yourself) quite literally, and someone editing the package might not realize the downstream consequences. To pull information from `package.json` in `plugins/openapi.js`: ```javascript filename="plugins/openapi.js" mark=9:11 import swagger from "@fastify/swagger"; import fp from "fastify-plugin"; import packageJson from "../package.json"; export default fp(async (fastify) => { fastify.register(swagger, { openapi: { info: { title: packageJson.name, description: packageJson.description, version: packageJson.version, //... }, }, }); }); ``` ## Update Fastify to Generate OpenAPI Component Schemas Fastify handles validation and serialization for Fastify apps based on schemas defined as JSON Schema but does not enforce separating schemas into reusable components. Let's start with a hypothetical example in a route definition, `routes/drink/index.js`: ```javascript filename="routes/drink/index.js" mark=11:16 export default async function (fastify, opts) { const schema = { params: { type: "object", properties: { drinkId: { type: "string" }, }, }, response: { 200: { type: "object", properties: { id: { type: "string" }, name: { type: "string" }, description: { type: "string" }, }, }, }, }; fastify.get("/:drinkId/", { schema }, async function (request, reply) { const { drinkId } = request.params; return { id: drinkId, name: "Example Drink Name", description: "Example description", }; }); } ``` The example above would generate the following OpenAPI schema for this route: ```json { "paths": { "/drink/{drinkId}/": { "get": { "parameters": [ { "schema": { "type": "string" }, "in": "path", "name": "drinkId", "required": true } ], "responses": { "200": { "description": "Default Response", "content": { "application/json": { "schema": { "type": "object", "properties": { "id": { "type": "string" }, "name": { "type": "string" }, "description": { "type": "string" } } } } } } } } } } } ``` Note how the response schema is presented inline. If we defined the schema for another route that returns a drink object similarly, our OpenAPI spec, resulting SDK, and documentation would not present a drink as a reusable component schema. Fastify provides methods to add and reuse schemas in an application. As a start, let's separate the response schema and use the Fastify `addSchema` method: ```javascript filename="routes/drink/index.js" mark=2:9,30 export default async function (fastify, opts) { fastify.addSchema({ $id: "Drink", type: "object", properties: { name: { type: "string" }, description: { type: "string" }, }, }); const schema = { params: { type: "object", properties: { drinkId: { type: "string" }, }, }, response: { 200: { $ref: "Drink", }, }, }; fastify.get("/:drinkId/", { schema }, async function (request, reply) { const { drinkId } = request.params; return { id: drinkId, name: "Example Drink Name", description: "Example description", }; }); } ``` We added a field called `$id` to our drink schema, then called `fastify.addSchema()` to add this shared schema to the Fastify app. To use this shared schema, we reference it using the JSON Schema `$ref` keyword, referencing the shared schema `$id` field. This generates the following OpenAPI schema: ```json { "components": { "schemas": { "def-0": { "type": "object", "properties": { "name": { "type": "string" }, "description": { "type": "string" } }, "title": "Drink" } } }, "paths": { "/drink/{drinkId}/": { "get": { "parameters": [ { "schema": { "type": "string" }, "in": "path", "name": "drinkId", "required": true } ], "responses": { "200": { "description": "Default Response", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/def-0" } } } } } } } } } ``` Note how, instead of defining the response schema inline with the path schema, we now have a component schema `def-0`, which our path's response schema references as `#/components/schemas/def-0`. This is already more useful. But if we were to generate an SDK or documentation based on this schema, the autogenerated name `def-0` would lead to documentation and methods for a schema component named `def-0`. Our next task is to customize this name. ## Creating Useful OpenAPI "$ref" references in Fastify By default, Fastify keeps track of all schemas added with `fastify.addSchema()` and numbers them. The default internal [function that builds these references](https://github.com/fastify/fastify-swagger/blob/ff0260811c0c319f4973665fd15517937e287040/lib/mode/dynamic.js#L17) looks like this: ```javascript filename="fastify-swagger/lib/mode/dynamic.js" function buildLocalReference(json, baseUri, fragment, i) { if (!json.title && json.$id) { json.title = json.$id; } return `def-${i}`; } ``` This function makes it clear where the `def-0` reference in our generated OpenAPI specification came from. Fastify allows us to override the `buildLocalReference` function as part of our OpenAPI options object in `plugins/openapi.js`: ```javascript filename="plugins/openapi.js" mark=13:17 import swagger from "@fastify/swagger"; import fp from "fastify-plugin"; export default fp(async (fastify) => { fastify.register(swagger, { openapi: { info: { title: "Speakeasy Bar API", description: "This is a sample API for Speakeasy Bar.", version: "1.0.1", }, }, refResolver: { buildLocalReference(json, baseUri, fragment, i) { return json.$id || `id-${i}`; }, }, }); }); ``` By overriding `buildLocalReference` in the snippet above, we help Fastify to use the `$id` field as the component schema's reference. If we were to regenerate the OpenAPI spec now, we would see that `def-0` is replaced by `Drink`. ## Customizing OpenAPI "operationId" Using Fastify Each path's `operationId` field in the OpenAPI specification is used to generate method names and documentation in SDKs. To add `operationId` to a route, add the field to the route's schema. For example: ```javascript filename="routes/drink/index.js" mark=3 fastify.get( "/:drinkId/", { schema: { operationId: "getDrink" } }, async function ({ params: { drinkId } }) { return { id: drinkId, }; }, ); ``` This would generate the following OpenAPI schema: ```json { "/drink/{drinkId}/": { "get": { "operationId": "getDrink", "responses": { "200": { "description": "Default Response" } } } } } ``` ## Add OpenAPI Tags to Fastify Routes At Speakeasy, whether you're building a big application or only have a handful of operations, we recommend adding tags to all your Fastify routes so you can group them by tag in generated SDK code and documentation. ### Add OpenAPI Tags to Routes in Fastify To add OpenAPI tags to a route in Fastify, add the `tags` keyword with a list of tags to the route's schema. Here's a simplified example from `routes/drink/index.js`: ```javascript filename="routes/drink/index.js" mark=3 fastify.get( "/:drinkId/", { schema: { tags: ["drinks"] } }, async function ({ params: { drinkId } }) { return { id: drinkId, }; }, ); ``` ### Add Metadata to Tags We can add a description and external documentation link to each tag by adding a list of tag objects to the Swagger options object in `plugins/openapi.js`: ```javascript filename="plugins/openapi.js" mark=8:17 import swagger from "@fastify/swagger"; import fp from "fastify-plugin"; export default fp(async (fastify) => { fastify.register(swagger, { openapi: { info: { tags: [ { name: "drinks", description: "Drink-related endpoints", externalDocs: { description: "Find out more", url: "http://swagger.io", }, }, ], }, }, }); }); ``` As with the other keys in the `info` options, Fastify copies the list of tags to the generated OpenAPI spec verbatim. ## Add a List of Servers to the Fastify OpenAPI Spec When validating an OpenAPI spec, Speakeasy expects a list of servers at the root of the spec. We'll add this to our options object in `plugins/openapi.js`: ```javascript filename="plugins/openapi.js" mark=7:12 import swagger from "@fastify/swagger"; import fp from "fastify-plugin"; export default fp(async (fastify) => { fastify.register(swagger, { openapi: { servers: [ { url: "http://localhost", description: "Development server", }, ], }, }); }); ``` ## Add Retries to Your SDK With "x-speakeasy-retries" If you are using Speakeasy to generate your SDK, we can customize it to follow custom rules for retrying failed requests. For instance, if your server fails to return a response within a specified time, you may want your users to retry their request without clobbering your server. Add retries to SDKs generated by Speakeasy by adding a top-level `x-speakeasy-retries` schema to your OpenAPI spec. You can also override the retry strategy per operation by adding `x-speakeasy-retries`. ### Adding Global Retries ```javascript filename="plugins/openapi.js" mark=7:17 import swagger from "@fastify/swagger"; import fp from "fastify-plugin"; export default fp(async (fastify) => { fastify.register(swagger, { openapi: { "x-speakeasy-retries": { strategy: "backoff", backoff: { initialInterval: 500, maxInterval: 60000, maxElapsedTime: 3600000, exponent: 1.5, }, statusCodes: ["5XX"], retryConnectionErrors: true, }, }, }); }); ``` Fastify respects OpenAPI extensions that start with `x-` and copies these to the root of the generated OpenAPI specification. ### Adding Retries per Method If we want to add a unique retry strategy to a single route, we can add `x-speakeasy-retries` to the route's schema: ```javascript filename="routes/drink/index.js" mark=5:15 fastify.get( "/:drinkId/", { schema: { "x-speakeasy-retries": { strategy: "backoff", backoff: { initialInterval: 500, maxInterval: 60000, maxElapsedTime: 3600000, exponent: 1.5, }, statusCodes: ["5XX"], retryConnectionErrors: true, }, }, }, async function (request, reply) { const { drinkId } = request.params; return { id: drinkId, name: "Example Drink Name", description: "Example description", }; }, ); ``` Once again, when generating an OpenAPI spec, Fastify will copy route-specific OpenAPI extensions without any changes. ## How To Generate an SDK Based on Your OpenAPI Spec Before generating an SDK, we need to save the Fastify-generated OpenAPI spec to a file. We'll add the following script to our `package.json` to generate `openapi.json` in the root of our project: ```json filename="package.json" { "scripts": { "openapi": "fastify generate-swagger app.js > openapi.json" } //... } ``` Then we run the following in the terminal: ```bash npm run openapi ``` After following the steps above, we have an OpenAPI spec that is ready to use as the basis for a new SDK. Now we'll use Speakeasy to generate an SDK. In the root directory of your project, run the following: ```bash speakeasy quickstart ``` Follow the onscreen prompts to provide the necessary configuration details for your new SDK such as the name, schema location and output path. Enter `openapi.json` when prompted for the OpenAPI document location and select TypeScript when prompted for which language you would like to generate. ## Add SDK Generation to Your GitHub Actions The Speakeasy [`sdk-generation-action`](https://github.com/speakeasy-api/sdk-generation-action) repository provides workflows that can integrate the Speakeasy CLI in your CI/CD pipeline, so your SDKs are regenerated when your OpenAPI spec changes. You can set up Speakeasy to automatically push a new branch to your SDK repositories so that your engineers can review and merge the SDK changes. For an overview of how to set up automation for your SDKs, see the Speakeasy documentation about [SDK Generation Action and Workflows](/docs/speakeasy-reference/workflow-file). ## Summary In this tutorial, we've learned how to generate an OpenAPI specification for your Fastify API. We also learn how to integrate Fastify with Speakeasy to generate SDKs. The tutorial guides you through step-by-step instructions on how to do this, from adding `@fastify/swagger` to a Fastify project and generating an OpenAPI specification to improving the generated OpenAPI specification for better SDK generation. It also covers how to use the Speakeasy OpenAPI extensions to improve generated SDKs and how to automate SDK generation as part of a CI/CD pipeline. Following these steps, you can successfully generate OpenAPI specifications for your Fastify app and improve your API operations. # How to generate an OpenAPI document with Flask Source: https://speakeasy.com/openapi/frameworks/flask import { Callout } from "@/mdx/components"; OpenAPI is a tool for defining and sharing REST APIs, and Flask can be paired with `flask-smorest` to build such APIs. This guide walks you through generating an OpenAPI document from a Flask project and using it to create SDKs with Speakeasy, covering the following steps: 1. Setting up a simple REST API with Flask 2. Integrating `flask-smorest` 3. Creating the OpenAPI document to describe the API 4. Customizing the OpenAPI schema 5. Using the Speakeasy CLI to create an SDK based on the schema 6. Integrating SDK creation into CI/CD workflows ## Requirements To follow along, you will need: - Python version 3.8 or higher - An existing Flask project or a copy of the provided [example repository](https://github.com/speakeasy-api/flask-openapi-example) - A basic understanding of Flask project structure and how REST APIs work ## Example Flask REST API repository The source code for the completed example is available in the [**Speakeasy Flask example repository**](https://github.com/speakeasy-api/openapi-flask-example). The repository already contains all the code covered throughout the guide. You can clone it and follow along with the tutorial, or use it as a reference to add to your own Flask project. To better understand the process of generating an OpenAPI document with Flask, let's start by inspecting some simple CRUD endpoints for an online library, along with a `Book` class and a serializer for our data. ### Models and routes ### Apps Open the `app.py` file, which serves as the main entry point of the program, and inspect the main function: ```python from flask import Flask from flask_smorest import Api from db import db import models from resources import blp as BookBlueprint import yaml app = Flask(__name__) app.config["API_TITLE"] = "Library API" app.config["API_VERSION"] = "v0.0.1" app.config["OPENAPI_VERSION"] = "3.1.0" app.config["OPENAPI_DESCRIPTION"] = "A simple library API" app.config["OPENAPI_URL_PREFIX"] = "/" app.config["OPENAPI_SWAGGER_UI_PATH"] = "/swagger-ui" app.config["OPENAPI_SWAGGER_UI_URL"] = "https://cdn.jsdelivr.net/npm/swagger-ui-dist/" app.config["SQLALCHEMY_DATABASE_URI"] = "sqlite:///database-file.db" app.config["API_SPEC_OPTIONS"] = {"x-speakeasy-retries": { 'strategy': 'backoff', 'backoff': { 'initialInterval': 500, 'maxInterval': 60000, 'maxElapsedTime': 3600000, 'exponent': 1.5, }, 'statusCodes': ['5XX'], 'retryConnectionErrors': True, } } db.init_app(app) api = Api(app) api.register_blueprint(BookBlueprint) # Add server information to the OpenAPI spec api.spec.options["servers"] = [ { "url": "http://127.0.0.1:5000", "description": "Local development server" } ] # Serve OpenAPI spec document endpoint for download @app.route("/openapi.yaml") def openapi_yaml(): spec = api.spec.to_dict() return app.response_class( yaml.dump(spec, default_flow_style=False), mimetype="application/x-yaml" ) if __name__ == "__main__": with app.app_context(): db.create_all() # Create database tables app.run(debug=True) ``` ### Database Here, you will see a method call to create a SQLite database and a function to run the Flask app: ```python mark=54,55 if __name__ == "__main__": with app.app_context(): db.create_all() # Create database tables app.run(debug=True) ``` ### Models From the root of the repository, open the `models.py` file to see a `Book` model containing a few fields with validation: ```python from db import db class Book(db.Model): __tablename__ = "books" id = db.Column(db.Integer, primary_key=True) title = db.Column(db.String(80), nullable=False) author = db.Column(db.String(80), nullable=False) description = db.Column(db.String(200)) ``` ### Schemas In the `schemas.py` file, the `BookSchema` class can be used to serialize and deserialize book data with the `marshmallow` package: ```python from marshmallow import Schema, fields class BookSchema(Schema): id = fields.Int(dump_only=True) title = fields.Str(required=True) author = fields.Str(required=True) description = fields.Str() ``` ### Resources The `resources.py` file contains API endpoints set up to handle all the CRUD operations for the books: ```python from flask.views import MethodView from flask_smorest import Blueprint, abort from sqlalchemy.exc import IntegrityError from db import db from models import Book from schemas import BookSchema blp = Blueprint("Books", "books", url_prefix="/books", description="Operations on books") @blp.route("/") class BookList(MethodView): @blp.response(200, BookSchema(many=True)) @blp.paginate() def get(self, pagination_parameters): """List all books""" query = Book.query paginated_books = query.paginate( page=pagination_parameters.page, per_page=pagination_parameters.page_size, error_out=False ) pagination_parameters.item_count = paginated_books.total return paginated_books.items @blp.arguments(BookSchema) @blp.response(201, BookSchema) def post(self, new_data): """Create a new book""" book = Book(**new_data) db.session.add(book) db.session.commit() return book @blp.route("/") class BookDetail(MethodView): @blp.response(200, BookSchema) def get(self, book_id): """Return books based on ID. --- Internal comment not meant to be exposed. """ book = Book.query.get_or_404(book_id) return book @blp.arguments(BookSchema) @blp.response(200, BookSchema) def put(self, updated_data, book_id): """Update an existing book""" book = Book.query.get_or_404(book_id) book.title = updated_data["title"] book.author = updated_data["author"] book.description = updated_data.get("description") db.session.commit() return book def delete(self, book_id): """Delete a book""" book = Book.query.get_or_404(book_id) db.session.delete(book) db.session.commit() return {"message": "Book deleted"}, 204 ``` This code defines a simple Flask REST API with CRUD operations for a `Book` model. The `BookList` class provides a way to retrieve all book data and create new books. The `BookDetail` class handles the retrieval of specific books, updating book data, and deleting books. ## Generate the OpenAPI document using `flask-smorest` Flask does not support OpenAPI document generation out-of-the-box, so we'll use the `flask-smorest` package to generate the OpenAPI document. If you are following along with the example repository, you can create and activate a virtual environment to install the project dependencies: ```bash filename="Terminal" python -m venv venv source venv/bin/activate pip install -r requirements.txt ``` If you have not already, install `flask-smorest` with the following command: ```bash filename="Terminal" pip install flask-smorest ``` ### Configuration The most basic configuration for generating an OpenAPI document with `flask-smorest` is added in the `app.py` file: ```python app.config["API_TITLE"] = "Library API" app.config["API_VERSION"] = "v0.0.1" app.config["OPENAPI_VERSION"] = "3.1.0" app.config["OPENAPI_DESCRIPTION"] = "A simple library API" ``` ### SwaggerUI The new Swagger UI endpoint is also added in the `app.py` file: ```python mark=14 app.config["OPENAPI_SWAGGER_UI_PATH"] = "/swagger-ui" ``` ### Server The `app.py` file contains additional configuration settings for the OpenAPI document. These add a development server: ```python # Add server information to the OpenAPI spec api.spec.options["servers"] = [ { "url": "http://127.0.0.1:5000", "description": "Local development server" } ] ``` ### Routes These additional configuration settings add a route to serve the OpenAPI document: ```python # Serve OpenAPI spec document endpoint for download @app.route("/openapi.yaml") def openapi_yaml(): spec = api.spec.to_dict() return app.response_class( yaml.dump(spec, default_flow_style=False), mimetype="application/x-yaml" ) ``` ### Run server To inspect and interact with the OpenAPI document, you need to run the development server, which will create a database file if one does not already exist, and serve the API. Run the development server: ```bash filename="Terminal" python app.py ``` You can now access the API and documentation: - Visit `http://127.0.0.1:5000/swagger-ui` to view the Swagger documentation and interact with the API. - Visit `http://127.0.0.1:5000/openapi.yaml` to download the OpenAPI document. ### OpenAPI document generation Now that we understand our Flask REST API, we can run the following command to generate the OpenAPI document using `flask-smorest`: ```bash filename="Terminal" flask openapi write --format=yaml openapi.yaml ``` This generates a new file, `openapi.yaml`, in the root of the project. ### Document Here, you can see an example of the generated OpenAPI document: ```yaml components: responses: DEFAULT_ERROR: content: application/json: schema: $ref: '#/components/schemas/Error' description: Default error response UNPROCESSABLE_ENTITY: content: application/json: schema: $ref: '#/components/schemas/Error' description: Unprocessable Entity schemas: Book: properties: author: type: string description: type: string id: readOnly: true type: integer title: type: string required: - author - title type: object Error: properties: code: description: Error code type: integer errors: additionalProperties: {} description: Errors type: object message: description: Error message type: string status: description: Error name type: string type: object PaginationMetadata: properties: first_page: type: integer last_page: type: integer next_page: type: integer page: type: integer previous_page: type: integer total: type: integer total_pages: type: integer type: object info: title: Library API version: v0.0.1 openapi: 3.1.0 paths: /books/: get: responses: '200': content: application/json: schema: items: $ref: '#/components/schemas/Book' type: array description: OK default: $ref: '#/components/responses/DEFAULT_ERROR' summary: List all books tags: - Books post: requestBody: content: application/json: schema: $ref: '#/components/schemas/Book' required: true responses: '201': content: application/json: schema: $ref: '#/components/schemas/Book' description: Created '422': $ref: '#/components/responses/UNPROCESSABLE_ENTITY' default: $ref: '#/components/responses/DEFAULT_ERROR' summary: Create a new book tags: - Books /books/{book_id}: delete: responses: default: $ref: '#/components/responses/DEFAULT_ERROR' summary: Delete a book tags: - Books get: responses: '200': content: application/json: schema: $ref: '#/components/schemas/Book' description: OK default: $ref: '#/components/responses/DEFAULT_ERROR' summary: Return books based on ID. tags: - Books parameters: - in: path name: book_id required: true schema: minimum: 0 type: integer put: requestBody: content: application/json: schema: $ref: '#/components/schemas/Book' required: true responses: '200': content: application/json: schema: $ref: '#/components/schemas/Book' description: OK '422': $ref: '#/components/responses/UNPROCESSABLE_ENTITY' default: $ref: '#/components/responses/DEFAULT_ERROR' summary: Update an existing book tags: - Books servers: - description: Local development server url: http://127.0.0.1:5000 tags: - description: Operations on books name: Books x-speakeasy-retries: backoff: exponent: 1.5 initialInterval: 500 maxElapsedTime: 3600000 maxInterval: 60000 retryConnectionErrors: true statusCodes: - 5XX strategy: backoff ``` ### Config Return to the `app.py` file to see how the app configuration influences the OpenAPI document generation: ```python app.config["API_TITLE"] = "Library API" app.config["API_VERSION"] = "v0.0.1" app.config["OPENAPI_VERSION"] = "3.1.0" app.config["OPENAPI_DESCRIPTION"] = "A simple library API" app.config["OPENAPI_URL_PREFIX"] = "/" app.config["OPENAPI_SWAGGER_UI_PATH"] = "/swagger-ui" app.config["OPENAPI_SWAGGER_UI_URL"] = "https://cdn.jsdelivr.net/npm/swagger-ui-dist/" ``` ### Metadata Open the `openapi.yaml` file to see the titles and versions reflected in the generated OpenAPI document: ```yaml info: title: Library API version: v0.0.1 openapi: 3.1.0 ``` ### ServerInfo The server URL is also included in the OpenAPI document: ```yaml servers: - description: Local development server url: http://127.0.0.1:5000 ``` ### BookParams Open the `models.py` file to see the `Book` parameters: ```python class Book(db.Model): __tablename__ = "books" id = db.Column(db.Integer, primary_key=True) title = db.Column(db.String(80), nullable=False) author = db.Column(db.String(80), nullable=False) description = db.Column(db.String(200)) ``` ### SchemaRef Open the `openapi.yaml`file to check the same `Book` parameters are reflected in the OpenAPI document: ```yaml schemas: Book: properties: author: type: string description: type: string id: readOnly: true type: integer title: type: string required: - author - title type: object ``` ## OpenAPI document customization The OpenAPI document generated by `flask-smorest` may not fit all use cases. The document can be customized further to better serve information about your API endpoints. You can add descriptions, tags, examples, and more to make the documentation more informative and user-friendly. In the [customized](https://github.com/speakeasy-api/openapi-flask-example/tree/customized) branch of the example repository, you can find a customized OpenAPI document that demonstrates the options available for modifying your generated document. ### Endpoints Open the `resources.py` file and inspect the configured endpoints: ```python from flask.views import MethodView from flask_smorest import Blueprint, abort from sqlalchemy.exc import IntegrityError from db import db from models import Book from schemas import BookSchema blp = Blueprint("Books", "books", url_prefix="/books", description="Operations on books") @blp.route("/") class BookList(MethodView): @blp.response(200, BookSchema(many=True)) @blp.paginate() def get(self, pagination_parameters): """List all books""" query = Book.query paginated_books = query.paginate( page=pagination_parameters.page, per_page=pagination_parameters.page_size, error_out=False ) pagination_parameters.item_count = paginated_books.total return paginated_books.items @blp.arguments(BookSchema) @blp.response(201, BookSchema) def post(self, new_data): """Create a new book""" book = Book(**new_data) db.session.add(book) db.session.commit() return book @blp.route("/") class BookDetail(MethodView): @blp.response(200, BookSchema) def get(self, book_id): """Return books based on ID. --- Internal comment not meant to be exposed. """ book = Book.query.get_or_404(book_id) return book @blp.arguments(BookSchema) @blp.response(200, BookSchema) def put(self, updated_data, book_id): """Update an existing book""" book = Book.query.get_or_404(book_id) book.title = updated_data["title"] book.author = updated_data["author"] book.description = updated_data.get("description") db.session.commit() return book def delete(self, book_id): """Delete a book""" book = Book.query.get_or_404(book_id) db.session.delete(book) db.session.commit() return {"message": "Book deleted"}, 204 ``` ### Responses You can indicate the expected response codes and models using `@blp.response()`: ```python @blp.response(200, BookSchema(many=True)) ``` ### OpenAPIResponse This results in the following additions, for example, to the `/books/` `get` operation in the OpenAPI document: ```yaml /books/: get: responses: '200': content: application/json: schema: items: $ref: '#/components/schemas/Book' type: array description: OK default: $ref: '#/components/responses/DEFAULT_ERROR' summary: List all books tags: - Books ``` ### Arguments Use the `@blp.arguments()` decorator to enforce a schema for arguments: ```python @blp.arguments(BookSchema) ``` ### OpenAPIArguments An enforced arguments schema results in the following additions to the `post` operation: ```yaml requestBody: content: ``` ### Pagination Allow pagination with the `@blp.paginate()` decorator: ```python @blp.paginate() ``` ### PaginationQuery Allowing paginations gives you access to the `page` and `page_size` properties, which you can use in your database query: ```python def get(self, pagination_parameters): """List all books""" query = Book.query paginated_books = query.paginate( page=pagination_parameters.page, per_page=pagination_parameters.page_size, error_out=False ) pagination_parameters.item_count = paginated_books.total return paginated_books.items ``` ### Docstrings You can add inline documentation using docstrings: ```python def get(self, book_id): """Return books based on ID. --- Internal comment not meant to be exposed. """ ``` ### DocReflection Docstrings are reflected in the OpenAPI document as follows: ```yaml summary: Return books based on ID. ``` ### Comments Notice the internal comment that is omitted from the OpenAPI document: ```python Internal comment not meant to be exposed. ``` ### Retries You can add global retries to the OpenAPI document by modifying the app config in the `app.py` file: ```python app.config["API_SPEC_OPTIONS"] = {"x-speakeasy-retries": { 'strategy': 'backoff', 'backoff': { 'initialInterval': 500, 'maxInterval': 60000, 'maxElapsedTime': 3600000, 'exponent': 1.5, }, 'statusCodes': ['5XX'], 'retryConnectionErrors': True, } } ``` ### SDKRetries This enables retries when using the document to create an SDK with Speakeasy: ```yaml x-speakeasy-retries: backoff: exponent: 1.5 initialInterval: 500 maxElapsedTime: 3600000 maxInterval: 60000 retryConnectionErrors: true statusCodes: - 5XX strategy: backoff ``` ## Creating SDKs for a Flask REST API To create a Python SDK for the Flask REST API, run the following command: ```bash filename="Terminal" speakeasy quickstart ``` Follow the onscreen prompts to provide the configuration details for your new SDK, such as the name, schema location, and output path. When prompted, enter `openapi.yaml` for the OpenAPI document location, select a language, and generate. ## Add SDK generation to your GitHub Actions The Speakeasy [`sdk-generation-action`](https://github.com/speakeasy-api/sdk-generation-action) repository provides workflows for integrating the Speakeasy CLI into your CI/CD pipeline, so that your SDKs are recreated whenever your OpenAPI document changes. You can set up Speakeasy to automatically push a new branch to your SDK repositories for your engineers to review before merging the SDK changes. For an overview of how to set up automation for your SDKs, see the Speakeasy [SDK Generation Action and Workflows](/docs/speakeasy-reference/workflow-file) documentation. ## SDK customization After creating your SDK with Speakeasy, you will find a new directory containing the generated SDK code, which we will now explore further. These examples assume a Python SDK named `books-python` was generated from the example Flask project above. Edit any paths to reflect your environment if you want to follow in your own project. ### BookClass Navigate into the `books-python/src/books/models` directory and find the `book.py` file created by Speakeasy. Note how the OpenAPI document was used to create the `Book` class: ```python """Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT.""" from __future__ import annotations from books.types import BaseModel from typing import Optional from typing_extensions import NotRequired, TypedDict class BookTypedDict(TypedDict): author: str title: str description: NotRequired[str] id: NotRequired[int] class Book(BaseModel): author: str title: str description: Optional[str] = None id: Optional[int] = None ``` ### SDKMethods Open the `src/books/books_sdk.py` file to see the methods that call the web API from an application using the SDK: ```python mark=13 class BooksSDK(BaseSDK): r"""Operations on books""" def get_books_( self, *, page: Optional[int] = 1, page_size: Optional[int] = 10, retries: OptionalNullable[utils.RetryConfig] = UNSET, server_url: Optional[str] = None, timeout_ms: Optional[int] = None, ) -> models.GetBooksResponse: r"""List all books :param page: :param page_size: :param retries: Override the default retry configuration for this method :param server_url: Override the default server URL for this method :param timeout_ms: Override the default request timeout configuration for this method in milliseconds """ base_url = None url_variables = None if timeout_ms is None: timeout_ms = self.sdk_configuration.timeout_ms if server_url is not None: base_url = server_url request = models.GetBooksRequest( page=page, page_size=page_size, ) req = self.build_request( method="GET", path="/books/", base_url=base_url, url_variables=url_variables, request=request, request_body_required=False, request_has_path_params=False, request_has_query_params=True, user_agent_header="user-agent", accept_header_value="application/json", timeout_ms=timeout_ms, ) if retries == UNSET: if self.sdk_configuration.retry_config is not UNSET: retries = self.sdk_configuration.retry_config else: retries = utils.RetryConfig( "backoff", utils.BackoffStrategy(500, 60000, 1.5, 3600000), True ) retry_config = None if isinstance(retries, utils.RetryConfig): retry_config = (retries, ["5XX"]) http_res = self.do_request( hook_ctx=HookContext( operation_id="get_/books/", oauth2_scopes=[], security_source=None ), request=req, error_status_codes=["422", "4XX", "5XX"], retry_config=retry_config, ) data: Any = None if utils.match_response(http_res, "200", "application/json"): return models.GetBooksResponse( result=utils.unmarshal_json(http_res.text, List[models.Book]), headers=utils.get_response_headers(http_res.headers), ) if utils.match_response(http_res, "422", "application/json"): data = utils.unmarshal_json(http_res.text, models.Error1Data) raise models.Error1(data=data) if utils.match_response(http_res, ["4XX", "5XX"], "*"): http_res_text = utils.stream_to_text(http_res) raise models.APIError( "API error occurred", http_res.status_code, http_res_text, http_res ) if utils.match_response(http_res, "default", "application/json"): return models.GetBooksResponse( result=utils.unmarshal_json(http_res.text, models.Error), headers={} ) content_type = http_res.headers.get("Content-Type") http_res_text = utils.stream_to_text(http_res) raise models.APIError( f"Unexpected response received (code: {http_res.status_code}, type: {content_type})", http_res.status_code, http_res_text, http_res, ) ``` ### Requests Here, you can see how the request to the API endpoint is built: ```python req = self.build_request( method="GET", path="/books/", base_url=base_url, url_variables=url_variables, request=request, request_body_required=False, request_has_path_params=False, request_has_query_params=True, user_agent_header="user-agent", accept_header_value="application/json", timeout_ms=timeout_ms, ) ``` ### RetriesConfig Finally, note the result of the global retries strategy that we set up in the `app.py` file: ```python retries = utils.RetryConfig( "backoff", utils.BackoffStrategy(500, 60000, 1.5, 3600000), True ) ``` ## Summary In this guide, we showed you how to generate an OpenAPI document for a Flask API and use Speakeasy to create an SDK based on the OpenAPI document. The step-by-step instructions included adding relevant tools to the Flask project, generating an OpenAPI document, enhancing it for improved creation, using Speakeasy CLI to create the SDKs, and interpreting the basics of the generated SDK. We also explored automating SDK generation through CI/CD workflows and improving API operations. # How to generate an OpenAPI/Swagger spec with Gnostic Source: https://speakeasy.com/openapi/frameworks/gnostic In this tutorial, we'll start with an OpenAPI document and end with a fully functional gRPC server. We'll also create a RESTful gateway to the gRPC server, and create SDKs in multiple languages. You might rightfully ask why we would want to do all of this, and there's no better way to illustrate a need than by starting with a real example that is entirely plausible and definitely not made up. Picture this: You're tasked with setting up an underground bar in the far reaches of the galaxy. Interstellar travelers from far and wide look forward to drinks they've never even dreamt of. Because this is the far future and, obviously, everyone is still using gRPC, we better set up a gRPC server that can handle billions of requests from light years away. gRPC helps us keep throughput high and latency low—both essential elements of an interstellar API. However, even civilizations that have mastered the Dyson Sphere know better than to use gRPC in the browser and other clients. So we'll need to create an HTTP server to complement our gRPC server. Our users will need SDKs to query our endpoints. There's no AI in the Laniakea Supercluster of galaxies who'd be willing to code all of this token by token, so let's help them generate as much of this as possible. We're working with enough acronyms to make our heads spin faster than the neutron star and the space jokes aren't helping. Let's break this down step by step and park the science fiction for the moment. Here's what we'll do: 1. Create an OpenAPI document describing our API. 2. Set up a development environment using Docker and dev containers. 3. Install a handful of dependencies. 4. Use Gnostic to generate a binary protocol buffer description of our API. 5. Use the Gnostic gRPC plugin to generate an annotated protocol buffer description of our API. 6. Transcode that description to create a gRPC API. 7. Create our server logic as a Go package. 8. Generate a gRPC gateway to handle HTTP requests and pass these to our server. 9. Use Speakeasy to create SDKs in Python and TypeScript. 10. And finally, test all of this by requesting some spectacular drinks. ## Example gRPC and REST API Server Repository The source code for our complete example is available in the [**Speakeasy gRPC and REST example repository**](https://github.com/speakeasy-api/grpc-rest-service). This repository already contains all the generated code we'll cover in this tutorial. You can clone it and follow along with the tutorial, or use it as a reference to build your own gRPC and REST API server. ## Creating an OpenAPI Document to Describe an API As a start, and for the sake of shipping our server, we'll create an API with only two endpoints. The first endpoint is `createDrink`: Create a new drink based on the provided ingredients and return the drink's name, description, recipe, and possibly a photo. Our second endpoint is `getDrink`: Create a new drink based only on the drink's name. Return a list of ingredients with quantities, a recipe, and a photo. ## Creating the OpenAPI Document Let's take a detailed tour of our API by exploring `bar.yaml`. ### OpenAPI Version We'll start by creating an OpenAPI 3.0.0 document. Gnostic, unfortunately, only supports OpenAPI 3.0.0, so we'll have to make sure our document is compliant. ```yaml filename="bar.yaml" openapi: 3.0.0 ``` ### API Information Next, we'll create an info object that describes our API: ```yaml filename="bar.yaml" info: title: Intergalactic Bar API version: 1.0.0 description: "An API for a cosmic bar that serves drinks from across the galaxy." ``` ### Server Configuration Now let's define the servers where our API will be hosted: ```yaml filename="bar.yaml" servers: - url: http://localhost:8080 description: Local server ``` ### Endpoints Let's define the endpoints for our API: #### Create Drink Endpoint The `/create-drink` endpoint accepts a POST request with the ingredients: ```yaml filename="bar.yaml" paths: /create-drink: post: summary: Create a new drink based on ingredients operationId: createDrink description: "Supply a list of ingredients and get back a new drink recipe." requestBody: required: true content: application/json: schema: $ref: '#/components/schemas/IngredientsRequest' ``` The endpoint returns a drink response: ```yaml filename="bar.yaml" responses: 200: description: "Successfully created a new drink" content: application/json: schema: $ref: '#/components/schemas/DrinkResponse' 400: description: "Invalid request" content: application/json: schema: $ref: '#/components/schemas/error' ``` #### Get Drink Endpoint The `/get-drink` endpoint accepts a POST request with the drink name: ```yaml filename="bar.yaml" /get-drink: post: summary: Get a drink recipe by name operationId: getDrink description: "Supply a drink name and get back its recipe." requestBody: required: true content: application/json: schema: $ref: '#/components/schemas/DrinkNameRequest' ``` The endpoint returns a drink recipe: ```yaml filename="bar.yaml" responses: 200: description: "Successfully retrieved the drink recipe" content: application/json: schema: $ref: '#/components/schemas/DrinkRecipeResponse' 400: description: "Invalid request" content: application/json: schema: $ref: '#/components/schemas/error' 404: description: "Drink not found" content: application/json: schema: $ref: '#/components/schemas/error' ``` ### Component Schemas Let's define the schemas for our components: #### Ingredients Request ```yaml filename="bar.yaml" components: schemas: IngredientsRequest: type: object description: "A request to create a new drink based on a list of ingredients." required: - ingredients_request properties: ingredients_request: type: object required: - ingredients properties: ingredients: type: array items: type: string description: "A list of ingredients to include in the drink." ``` #### Drink Response ```yaml filename="bar.yaml" DrinkResponse: type: object description: "A response containing a new drink recipe." required: - name - description - recipe properties: name: type: string description: "The name of the drink." description: type: string description: "A description of the drink." recipe: type: string description: "Instructions on how to make the drink." photo: type: string description: "A URL to a photo of the drink." ``` #### Drink Name Request ```yaml filename="bar.yaml" DrinkNameRequest: type: object description: "A request to get a drink recipe by name." required: - drink_name_request properties: drink_name_request: type: object required: - name properties: name: type: string description: "The name of the drink." ``` #### Drink Recipe Response ```yaml filename="bar.yaml" DrinkRecipeResponse: type: object description: "A response containing a drink recipe." required: - ingredients - recipe properties: ingredients: type: array items: $ref: '#/components/schemas/IngredientQuantity' description: "A list of ingredients with quantities." recipe: type: string description: "Instructions on how to make the drink." photo: type: string description: "A URL to a photo of the drink." ``` #### Ingredient Quantity ```yaml filename="bar.yaml" IngredientQuantity: type: object description: "An ingredient with a quantity." required: - name - quantity properties: name: type: string description: "The name of the ingredient." quantity: type: string description: "The quantity of the ingredient." ``` #### Error ```yaml filename="bar.yaml" error: type: object description: "An error response." required: - code - message properties: code: type: integer description: "The error code." message: type: string description: "The error message." ``` ## Setting Up the Development Environment Here, we'll take an opinionated approach to setting up a development environment. We'll use Docker and dev containers in VS Code to ensure that everyone has the same environment and avoid any issues with dependencies. We'll start by creating a `Dockerfile` in the `.devcontainer` directory. Our development container is based on `mcr.microsoft.com/devcontainers/go` from the Microsoft Dev Container Images repository: ```docker filename="Dockerfile" FROM mcr.microsoft.com/devcontainers/go:1.22-bookworm RUN curl -o- https://raw.githubusercontent.com/speakeasy-api/speakeasy/main/install.sh | bash RUN go install github.com/google/gnostic@v0.7.0 && \ go install github.com/google/gnostic-grpc@latest && \ go install github.com/protocolbuffers/protobuf-go/cmd/protoc-gen-go@v1.32.0 && \ go install github.com/bufbuild/buf/cmd/buf@v1.30.0 && \ go install google.golang.org/grpc/cmd/protoc-gen-go-grpc@v1.3.0 && \ go install github.com/fullstorydev/grpcurl/cmd/grpcurl@latest && \ go install github.com/grpc-ecosystem/grpc-gateway/v2/protoc-gen-grpc-gateway@v2.18.0 COPY . /app WORKDIR /app RUN go mod tidy EXPOSE 50051 CMD ["go", "run", "main.go"] ``` In this Dockerfile: 1. We install Speakeasy using the `install.sh` script from the Speakeasy repository 2. We install all necessary tools: - Gnostic - Gnostic gRPC plugin - Go protocol buffer compiler - Buf - gRPC Go plugin - gRPCurl - gRPC gateway plugin 3. We copy our project files into the container and set the working directory to `/app` 4. We run `go mod tidy` to ensure all dependencies are up to date 5. We expose port 50051 for the gRPC server and set the command to run our server ### Dev Container Configuration Next, we'll create a `devcontainer.json` file in our `.devcontainer` directory to configure our development container: ```json filename=".devcontainer/devcontainer.json" { "name": "Go", "dockerComposeFile": [ "../docker-compose.yaml", "docker-compose.yaml" ], "service": "app", "workspaceFolder": "/app", "shutdownAction": "stopCompose" } ``` For help with the `devcontainer.json` file, check out the [official documentation](https://containers.dev/implementors/json_reference/). ### Docker Compose We'll also create a `docker-compose.yaml` file in our `.devcontainer` directory to define our development container: ```yaml filename=".devcontainer/docker-compose.yaml" version: "3" services: app: build: context: . dockerfile: .devcontainer/Dockerfile volumes: - ..:/workspaces:cached command: /bin/sh -c "while sleep 1000; do :; done" ``` This Docker Compose file defines a service called `app` that uses the `Dockerfile` in the `.devcontainer` directory. It also mounts the current directory into the container at `/app` and overrides the default command to start the server. We use `/bin/sh -c "while sleep 1000; do :; done"` as the default command to keep the container running while we work on our server. This means we can start the server manually when we're ready. ### Starting the Development Container For this step, you'll need to have [Docker](https://www.docker.com/get-started) and the [Dev Containers extension](https://marketplace.visualstudio.com/items?itemName=ms-vscode-remote.remote-containers) for VS Code installed. Start the Docker engine and open the project in VS Code. Press F1 to open the command palette, then select `Dev Containers: Reopen in Container`. This will build the development container and open a new VS Code window inside it. This step might take a while the first time you run it, as it needs to download the base image and install all the dependencies. ### Interacting With the Development Container Once the development container is running, you can interact with it using the terminal in VS Code. You can run commands as you would in a regular terminal, such as `go run main.go` to start the server later on. When we refer to running commands in the development container, we mean running them in the dev container terminal in VS Code. If you think something isn't working as expected, try restarting the development container by running `Dev Containers: Rebuild Container` from the command palette. You may also restart Docker to ensure everything is working as expected. ## Dependencies for Generating a gRPC Server Using Gnostic To generate a gRPC server using Gnostic, we added the following dependencies to our development container: - [Gnostic](https://github.com/google/gnostic): A compiler for APIs described by the OpenAPI 3.0.0 specification. - [Gnostic gRPC plugin](https://github.com/google/gnostic-grpc): A plugin for Gnostic that generates gRPC service definitions. - [Go protocol buffer compiler](https://pkg.go.dev/github.com/golang/protobuf/protoc-gen-go): A plugin for the protocol buffer compiler that generates Go code. - [Buf](https://buf.build/): A tool for managing protocol buffer files. - [gRPC Go plugin](https://pkg.go.dev/google.golang.org/grpc/cmd/protoc-gen-go-grpc): A plugin for the protocol buffer compiler that generates Go gRPC code. - [gRPCurl](https://github.com/fullstorydev/grpcurl): A command-line tool for interacting with gRPC servers. We'll use this to test our gRPC server. - [gRPC gateway plugin](https://github.com/grpc-ecosystem/grpc-gateway): A plugin for the protocol buffer compiler that generates a reverse proxy server to translate RESTful HTTP and JSON requests to gRPC. ## Generating a gRPC Server Using Gnostic To generate a gRPC server using Gnostic, we need to follow these steps: 1. Compile the API definition to a binary protocol buffer file using Gnostic. 2. Create an API definition in a `.proto` file. 3. Generate a gRPC service definition using the Gnostic gRPC plugin. 4. Generate Go code using the Go protocol buffer compiler and the gRPC Go plugin. We've already completed step 1 by creating the `bar.yaml` file. Now we'll compile the API definition to a binary protocol buffer file using Gnostic. ### Compiling the API Definition to a Binary Protocol Buffer File This step is necessary because we've found that the Go protocol buffer compiler and the gRPC Go plugin require a binary protocol buffer file as input. To compile the API definition to a binary protocol buffer file, we'll use the following command: ```bash filename="vscode@devcontainer" gnostic --pb-out=. bar.yaml ``` This command compiles the API definition in `bar.yaml` to a binary protocol buffer file named `bar.pb`. ### Creating an API Definition in a `.proto` File Next, we'll create an API definition in a `.proto` file. We'll use the `bar.pb` file generated in the previous step as input. To accomplish this, we'll use the following command: ```bash filename="vscode@devcontainer" gnostic-grpc -input bar.pb -output . ``` This command generates a `.proto` file named `bar.proto` that contains the gRPC service definition: ```cpp filename="bar.proto" syntax = "proto3"; package bar; import "google/api/annotations.proto"; import "google/protobuf/descriptor.proto"; import "google/protobuf/empty.proto"; message IngredientsRequest { repeated string ingredients = 1; } message DrinkResponse { string name = 1; string description = 2; string recipe = 3; string photo = 4; } message DrinkNameRequest { string name = 1; } message DrinkRecipeResponse { repeated IngredientQuantity ingredients = 1; string recipe = 2; string photo = 3; } message IngredientQuantity { string name = 1; string quantity = 2; } message Error { int32 code = 1; string message = 2; } //CreateDrinkParameters holds parameters to CreateDrink message CreateDrinkRequest { IngredientsRequest ingredients_request = 1; } //GetDrinkParameters holds parameters to GetDrink message GetDrinkRequest { DrinkNameRequest drink_name_request = 1; } service Bar { rpc CreateDrink ( CreateDrinkRequest ) returns ( DrinkResponse ) { option (google.api.http) = { post:"/create-drink" body:"ingredients_request" }; } rpc GetDrink ( GetDrinkRequest ) returns ( DrinkRecipeResponse ) { option (google.api.http) = { post:"/get-drink" body:"drink_name_request" }; } } ``` ### Generating Go Code Using Buf and the gRPC Go Plugin To set up Buf, we need to run the following command: ```bash filename="vscode@devcontainer" buf mod init ``` This command initializes a new Buf module in the current directory. Update the `buf.yaml` file created in the project root with the following content: ```yaml filename="buf.yaml" version: v1 deps: - buf.build/googleapis/googleapis ``` We depend on `googleapis` because the gRPC Go plugin requires it. ### Configuring Buf for Code Generation Next, create a `buf.gen.yaml` file in the project root with the following content: ```yaml filename="buf.gen.yaml" version: v1 managed: enabled: true go_package_prefix: default: github.com/speakeasy-api/grpc-rest-service/bar except: - buf.build/googleapis/googleapis plugins: - name: go out: . opt: paths=source_relative - name: go-grpc out: . opt: paths=source_relative,require_unimplemented_servers=false - name: grpc-gateway out: . opt: paths=source_relative ``` In this configuration: 1. We enable managed mode, which helps Buf manage the generated code 2. We set the `go_package_prefix` to `github.com/speakeasy-api/grpc-rest-service/bar`, which determines the package name in the generated Go code 3. We configure three plugins: - `go`: Generates Go code for the protocol buffers - `go-grpc`: Generates Go code for the gRPC service - `grpc-gateway`: Generates Go code for the gRPC gateway, which translates RESTful HTTP and JSON requests to gRPC Update Buf's dependencies using the following command: ```bash filename="vscode@devcontainer" buf mod update ``` Now we can generate Go code using the following command: ```bash filename="vscode@devcontainer" buf generate ``` This command generates Go code in the `bar` directory. The generated code includes the gRPC service definition and the gRPC gateway definition. ## Implementing the gRPC Server To implement the gRPC server, we created one big `main.go` with our endpoint implementations and business logic all in one. This will make a lot of people very angry and is widely regarded as a bad move. ([🫡 Douglas Adams](https://www.goodreads.com/quotes/1-the-story-so-far-in-the-beginning-the-universe-was)) To make things slightly more confusing, we're including OpenAI API calls alongside our focus on OpenAPI. The similarities in the names are purely coincidental. The `main.go` file is too large to include here, but you can find it in the root of the example project. ## Optional: Adding Your OpenAI API Key If you want to use the OpenAI API to generate drink recipes, you'll need to add your OpenAI API key to the project. Without this key, the server will return placeholder data for the drink recipes. Copy the `.env.template` file to a new file named `.env`: ```bash filename="vscode@devcontainer" cp .env.template .env ``` Open the `.env` file and add your OpenAI API key: ```bash OPENAI_API_KEY=your-openai-api-key ``` This file is included in the `.gitignore` file, so it won't be checked into version control. ## Rebuilding the Docker Image We've installed a bunch of new dependencies and generated a lot of new code. To make sure everything is working as expected, we need to rebuild our Docker image. This also runs `go mod tidy` to ensure all dependencies are up to date, which can take a while. In VS Code, press F1 to open the command palette, then select `Dev Containers: Rebuild Container`. This will rebuild the development container and install all the dependencies. ## Running the gRPC Server To run the gRPC server, we need to start the development container and run the following command: ```bash filename="vscode@devcontainer" go run main.go ``` This command starts the gRPC server on port `50051` and the gRPC gateway on port `8080`. ## Testing the gRPC Server To test the gRPC server, we'll use the `grpcurl` command-line tool. First, open a new terminal in VS Code by pressing ⌃⇧`. This next step will use OpenAI credits, so make sure you have credits available before running the command. Now run the following command to send a request to the gRPC server: ```bash filename="vscode@devcontainer" grpcurl -plaintext -d '{"drink_name_request": {"name": "Pan Galactic Gargle Blaster"}}' localhost:50051 bar.Bar/GetDrink ``` This command sends a request to the `GetDrink` endpoint with the `name` field set to `Pan Galactic Gargle Blaster`. The response should look something like this: ```json { "ingredients": [ { "name": "Ol' Janx Spirit", "quantity": "1 oz" }, { "name": "Water from the seas of Santraginus V", "quantity": "0.5 oz" }, { "name": "Arcturan Mega-gin", "quantity": "1 oz" }, { "name": "Fallian marsh gas", "quantity": "A gentle bubble" }, { "name": "Quantum hyper-mint extract", "quantity": "1 teaspoon" }, { "name": "Zap powder", "quantity": "A pinch" }, { "name": "Algolian Suntiger tooth extract", "quantity": "1 drop" }, { "name": "Galaxy-wide famous Olives", "quantity": "1 olive" } ], "recipe": "In a cosmic shaker, mix Ol' Janx Spirit, Arcturan Mega-gin, and water from Santraginus V. Gently add fallian marsh gas to create a mystery bubble. Stir in quantum hyper-mint extract and a pinch of zap powder with a molecular stirrer (mind the speed, or you'll end up in another dimension). Carefully add a single drop of Algolian Suntiger tooth extract, ensuring not to evaporate your mixing vessel. Serve in a glass forged from comets' ice, garnished with a galaxy-wide famous Olive. Be sure to have your propulsion system set to the nearest recovery planet because after one sip, you'll need it.", "photo": "https://example.com/photo.jpg" } ``` The photo URL returned by OpenAI is only valid for an hour, so be sure to open it in a browser to view it. You may find that the terminal output escapes the JSON response, breaking the photo URL. You can use the following command to unescape the JSON response: ```bash filename="vscode@devcontainer" grpcurl -plaintext -d '{"drink_name_request": {"name": "Pan Galactic Gargle Blaster"}}' localhost:50051 bar.Bar/GetDrink | sed 's/%3A/:/g; s/%2F/\//g; s/%3D/=/g; s/%3F/?/g; s/%26/\&/g' | jq ``` This command uses `sed` to unescape the JSON response and `jq` to format the JSON response. ## Generating SDKs for the gRPC Gateway To generate a TypeScript SDK for the gRPC gateway, we run the following command: ```bash filename="vscode@devcontainer" speakeasy quickstart ``` Follow the onscreen prompts to provide the necessary configuration details for your new SDK such as the name, schema location and output path. Enter `bar.yaml` when prompted for the OpenAPI document location and select TypeScript when prompted for which language you would like to generate. To generate a Python SDK for the gRPC gateway, we run the following command: ```bash filename="vscode@devcontainer" speakeasy quickstart ``` Follow the onscreen prompts to provide the necessary configuration details for your new SDK such as the name, schema location and output path. Enter `bar.yaml` when prompted for the OpenAPI document location and select Python when prompted for which language you would like to generate. Now we have generated SDKs for the gRPC gateway in both TypeScript and Python. # How to generate an OpenAPI/Swagger spec with Goa Source: https://speakeasy.com/openapi/frameworks/goa import { Callout } from "@/mdx/components"; This tutorial explains how to define an application programming interface (API) service written in Go with Goa, convert it to an OpenAPI schema with Goa, and convert that to software development kits (SDKs) in multiple languages with Speakeasy. (OpenAPI used to be known as Swagger, which is now a set of tools that can be used with OpenAPI schemas.) Below is a graphical summary of the creation process. ```mermaid flowchart LR design.go --> Goa Goa --> api[OpenAPI schema] Goa --> cli[Client API CLI] Goa --> client[Client Go code] Goa --> server[Server Go code] api --> Speakeasy Speakeasy --> ts[TypeScript SDK] Speakeasy --> c[C# SDK] Speakeasy --> p[Go SDK] Speakeasy --> etc[Other SDKs...] ``` We will talk you through creating a complete code example. By the end of the tutorial, you will have a working API service running in Go that you call through TypeScript code in an SDK. ## Is This Tutorial Right for You? If you're new to OpenAPI, Go, Goa, or Speakeasy, this is the perfect tutorial to see if they are the appropriate technologies for your service. If you're familiar with Goa, but not with Speakeasy, this is also the right place to start. But if you want to use your OpenAPI schema as a starting point, and not Go code that is transpiled into a schema, Goa is not the right choice for you. Rather, choose one of the other [Go frameworks](#other-go-openapi-frameworks) below. You also might want to design your schema without choosing any programming language. In this case, you could start with the [Swagger schema editor](https://swagger.io/tools/swagger-editor/). However, using the elegant Goa design language is a lot simpler than trying to design your schema manually. It also creates all the HTTP and gRPC transport code for you. ## Prerequisites For this tutorial, you only need Docker version 20 or newer. You can complete this tutorial on Linux, macOS, or Windows since Docker commands are not dependent on your operating system or any installed frameworks. If you are running on Windows, please replace backslashes with forward slashes in the few places where you specify a folder path on your host machine. ## Introduction to Goa The [OpenAPI specification](https://spec.openapis.org/oas/latest.html) is a standard that explains how to create a human- and machine-readable schema (`.json` or `.yaml` document) for any API service. (This tutorial refers to the standard as "the OpenAPI Specification", and the definition you create for your service as "your OpenAPI schema", or just "your schema".) [Goa](https://github.com/goadesign/goa?tab=readme-ov-file) is a package written in the Go language that allows you to define an API in Go syntax using functions from the Goa design language. Goa uses your definition in its `design.go` file to create: - An OpenAPI schema that can be used by programmers or tools like Speakeasy to understand your API. - Go code for a client application to call your API. - A command line interface (CLI) to call your API. - The transport-agnostic code to provide the API on a server over protocols like HTTP and gRPC. - Go code stubs for the service itself, which you can complete with business logic. ### Other Go OpenAPI Frameworks Most other Go frameworks generate code from an existing OpenAPI schema and don't allow you to write Go as a starting point. These include: - [Deepmap OpenAPI code generator](https://github.com/deepmap/oapi-codegen) - [Ogen](https://github.com/ogen-go/ogen) - [Swaggest OpenAPI structures for Go](https://github.com/swaggest/openapi-go) [Swaggest Rest](https://github.com/swaggest/rest) can generate OpenAPI definitions from Go code, but it's not as comprehensive as Goa and does not support gRPC. ### Is This Related to Tsoa? [Tsoa](https://github.com/lukeautry/tsoa) is a popular TypeScript framework similar to Goa that you may encounter in the OpenAPI ecosystem. Speakeasy has a [tutorial](/docs/api-frameworks/tsoa) for tsoa, too. Goa was created in 2015 and tsoa in 2016. While tsoa uses decorators and can work with normal Express code, Goa starts with an abstract design document in its domain-specific language (DSL) and uses that to generate code and schemas. ## Create the API Schema in Goa Now that you understand how Goa and Speakeasy are used, let's write some code. ### Download the Example Repository First, clone the [example repository](https://github.com/speakeasy-api/speakeasy-goa-example.git) using the code below. If you don't have Git, you can download the code and unzip it. ```bash git clone https://github.com/speakeasy-api/speakeasy-goa-example.git; cd speakeasy-goa-example; ``` While you will create a demonstration application in the `app` folder in this tutorial, there is a folder called `completed_app` in the example repository that has all the final generated code and executable files. ### Google Protocol Buffers Goa generates [gRPC](https://grpc.io/) code for you. gRPC is an efficient alternative to plain HTTP, over which you can provide your API. It requires the use of protocol buffers, made by Google. Our repository already provides the `protoc` app for you, in `completed_app/lib`. To use more recent versions of `protoc` in future applications, you can download them from [the Protobuf repository](https://github.com/protocolbuffers/protobuf/releases). ### Set Up Go First, set up your environment. You'll run a Go Docker container and later you'll install Node.js in it. In a terminal in the `speakeasy-goa-example` folder, run the commands below. Comments in the commands explain what they do. ```bash mkdir app; cd app; cp -r ../completed_app/lib .; # copy protoc into your new app cp -r ../completed_app/design .; # copy in a simple Goa design file docker run --name gobox --volume .:/go/src/app -it golang:1.21.2 bash; # start Go in a container and share your app folder with it ``` You now have an `app` folder ready to code in, and you're in a terminal in a Go container called `gobox` in Docker. If you leave this tutorial and return later, you can start the container and attach to the terminal instead of rebuilding everything: ```bash docker start gobox; docker exec -it gobox bash; export PATH=$PATH:/go/src/app/lib; cd /go/src/app; ``` If you need to delete the container and start over, run: ```bash docker stop gobox; docker rm gobox; ``` Run the following commands in the `gobox` terminal. ```bash cd /go/src/app; go mod init app; # create a new Go package in this folder called app go install goa.design/goa/v3/cmd/goa@v3; # install Goa go install google.golang.org/protobuf/cmd/protoc-gen-go@latest; # install gRPC go install google.golang.org/grpc/cmd/protoc-gen-go-grpc@latest; export PATH=$PATH:/go/src/app/lib; # add protoc to your path ``` ### Review the Goa Design File You now have Goa installed and ready to run against a Goa design file. Let's pause to review the API schema in `app/design/design.go`. Open the file now. After importing Goa, the design starts by defining the top-level API: ```go filename="design.go" var _ = API("club", func() { Title("The Speakeasy Club") Version("1.0.0") Description("A club that serves drinks and plays jazz. A Goa and Speakeasy example.") Contact(func() { Name("Speakeasy Support") URL("https://go.speakeasy.com/slack") }) Docs(func() { Description("The Speakeasy Club documentation") URL("https://www.speakeasy.com/docs") }) License(func() { Name("Apache 2.0") URL("https://www.apache.org/licenses/LICENSE-2.0.html") }) TermsOfService("https://www.speakeasy.com/docs/terms-of-service") Server("club", func() { Description("club server hosts the band and order services.") Services("band", "order") Host("dev", func() { Description("The development host. Safe to use for testing.") URI("http://{machine}:51000") // use the machine variable below URI("grpc://{machine}:52000") Variable("machine", String, "Machine IP Address", func() { Default("localhost") }) }) }) Meta("openapi:extension:x-speakeasy-retries", `{ "strategy":"backoff", "statusCodes": "408,504" }`) }) ``` Note that everything in the Goa DSL is a function. The `API` function takes a function that runs other functions to specify parts of the overall definition of the API, such as `Description`, `Version`, and server endpoints (URLs). To learn all the possible functions Goa provides, read the [DSL documentation](https://pkg.go.dev/goa.design/goa/dsl). To create a simpler, minimal definition, use the [Goa getting started guide](https://github.com/goadesign/goa?tab=readme-ov-file). The example you create here provides a virtual jazz club, allowing you to order a digital drink and change the genre of music played. These features are defined in two separate services, `order` and `band`. While the `club` API title corresponds to the server URL and is not visible, the service names are visible in the URLs `http://localhost:51000/order` and `http://localhost:51000/band`. The definitions of the two services are below the API. Let's look at the drinks service. ```go filename="design.go" var _ = Service("order", func() { Description("A waiter that brings drinks.") Method("tea", func() { Description("Order a cup of tea.") Payload(func() { Field(1, "isGreen", Boolean, "Whether to have green tea instead of normal.") Field(2, "numberSugars", Int, "Number of spoons of sugar.") Field(3, "includeMilk", Boolean, "Whether to have milk.") }) Result(String) HTTP(func() { Meta("openapi:tag:Drink operations") POST("/tea") }) GRPC(func() { }) }) Files("/openapi.json", "./gen/http/openapi.json") }) ``` Here you can see we've defined a single POST method called `tea` in the `order` service. The `tea` method takes a few parameters about how you like your milk and sugar and returns a string representing a cup of tea. Note that an HTTP GET method won't accept a complex method body, so you have to use POST for any calls other than simple URL IDs. #### Encapsulate With References Goa automatically moves complex types out of the service definition section and into their own section in the schema. For example, in the generated OpenAPI specification file, the line `$ref: '#/components/schemas/TeaRequestBody'` refers the order specification for tea to the `components` section later in the document using the JSON [references](https://swagger.io/docs/specification/using-ref) feature. #### Rename With Custom operationIds If you can't find a function in the Goa documentation that corresponds to an OpenAPI field you are expecting, you can probably add it with the [`Meta`](https://pkg.go.dev/goa.design/goa/dsl#Meta) function. The `Meta` function will add a field and value to any object. For example, here's how you can change the default `operationId` value for a method, which is normally `serviceName#methodPath`: ```go Method("play", func() { Meta("openapi:operationId", "band#play2") ``` An `operationId` is a unique name to clearly identify an operation (method). They are useful to name and discuss operations in documentation and SDKs. #### Group Operations With Tags Speakeasy recommends adding [tags](https://swagger.io/docs/specification/grouping-operations-with-tags/) to all operations so that you can group operations by tag in generated SDK code and documentation. A tag is just a label, like a comment, that you can add to a method. The [`Tag`](https://pkg.go.dev/goa.design/goa/dsl#Tag) function in Goa has a different meaning than it does in OpenAPI, so you need to use the `Meta` function again in the `HTTP` section of a method. ```go var _ = Service("band", func() { Method("play", func() { HTTP(func() { Meta("openapi:tag:Music operations") ``` ### Generate the API Code With Goa Generate the client and server code and your OpenAPI schema from your design file by running the commands below in the `gobox` container. ```bash goa gen app/design; goa example app/design; ``` The files created in the container will belong to the root user, and you will not be able to edit them on your host. In a terminal on your host machine, go to the `app` folder and give yourself permissions to edit the created files: ```bash sudo chown -R $(id -u):$(id -g) . ``` Rerun this whenever you create a file in the container. #### Explore the Generated Files Goa has written a lot of code for us. Below is everything created under `app` and what it does. To avoid duplication, there is only an explanation for the band files and not for the order files, because they are services with identical structures. The following files and folders are created by `goa gen`, and you may regenerate them when your `design.go` file changes. You may not edit them manually. - `/gen` — Contains all definition and communication code for HTTP, gRPC, and schemas. Think of the `gen` folder as your `definitions` folder. - `/gen/band` — Contains transport independent service code. - `/gen/band/client.go` — Can be imported by client code to make calls to the server. - `/gen/band/endpoints.go` — Exposes the service code to the transport layers. - `/gen/grpc` — Contains the server and client code that connects the `protoc`-generated gRPC server and client code, along with the logic to encode and decode requests and responses. - `/gen/grpc/band/pb` — Contains the protocol buffer files that describe the band gRPC service. - `/gen/grpc/band/pb/goagen_app_band_grpc.pb.go` — The output of the `protoc` tool. - `/gen/grpc/cli` — Contains the CLI code to build gRPC requests from a terminal. - `/gen/http` — All HTTP-related transport code, for server and client. - `/gen/http/openapi3.yaml` — The OpenAPI version 3 schema (next to `.json` and version 1 schemas). The following files and folders are created by `goa example` and you can use them as a starting point to write business logic implementation and tests for your server. You **should not** regenerate these files when your `design.go` file changes, rather update them manually. If you haven't started work on your implementation yet and do wish to regenerate the files, delete the existing files first to be certain that Goa recreates them. - `/cmd` — Contains working placeholder server and CLI code. Think of the `cmd` folder as your `implementation` folder. - `/cmd/club` — A Go package containing your API that you can compile and run to have a working server. - `/cmd/club/main.go` — The server implementation that you can change to your liking. Add your favorite logger and database manager here. - `/cmd/club-cli` — A Go package containing a CLI tool that you can compile and call from the terminal to make requests to the server above. - `band.go` — A placeholder implementation of your service. You can write your business logic here. You might think this file belongs in the `/cmd/club` folder with the API implementation instead of in the root of the project, but Goa puts the API implementation in the `cmd` folder and all service implementations in the root. Take a little time to review `/gen/http/openapi3.yaml` and see how your `design.go` functions map to the generated YAML. ### Run the Server and CLI Goa has given us a simple working client and server implementation. Let's compile and test them before starting with Speakeasy. Run the code below in the container. ```bash go get app/cmd/club; # download dependencies go build ./cmd/club && go build ./cmd/club-cli; # build the server ./club; # start the server ``` You now have two executable files called `club` and `club-cli` in the `app` folder. The Club server is running inside Docker. The output should look like this: ```bash [club] 16:20:55 HTTP "Play" mounted on POST /play [club] 16:20:55 HTTP "./gen/http/openapi.json" mounted on GET /openapi.json [club] 16:20:55 HTTP "Tea" mounted on POST /tea [club] 16:20:55 HTTP "./gen/http/openapi.json" mounted on GET /openapi.json [club] 16:20:55 serving gRPC method band.Band/Play [club] 16:20:55 serving gRPC method order.Order/Tea [club] 16:20:55 HTTP server listening on "localhost:51000" [club] 16:20:55 gRPC server listening on "localhost:52000" ``` Open another terminal on your host machine and log in to a new Docker terminal: ```bash docker exec -it gobox bash; cd /go/src/app; ./club-cli --help; # see if you can call the server from a CLI client ./club-cli order tea --body '{"includeMilk": false, "isGreen": false, "numberSugars": 1 }'; ./club-cli band play --body '{"style": "Bebop" }'; ``` While the CLI won't receive a response from the server because the implementation is just a placeholder, you can see in the server terminal that it has been successfully called. ## Customize With Speakeasy Extensions OpenAPI supports fields that are not in the specification. These [extensions](https://swagger.io/docs/specification/openapi-extensions/) allow you to add custom data to your schema that might have special meaning to applications like Speakeasy. They start with `x-`. Speakeasy provides a set of [OpenAPI extensions](/docs/customize-sdks). For example, you may want to give an SDK method a name different from the `operationId`: ```go Method("tea", func() { Meta("openapi:extension:x-speakeasy-name-override", "chai") ``` ### Retry Calling the Server Speakeasy provides a schema extension called [retries](https://www.speakeasy.com/docs/customize-sdks/retries) that will create an SDK that automatically retries calling the server if a call fails. In `design.go`, retries for timeout errors are enabled for the entire API rather than for an individual service or method. For example, this code: ```go Meta("openapi:extension:x-speakeasy-retries", `{ "strategy":"backoff", "statusCodes": "504,408" }`) ``` Will produce this OpenAPI YAML: ```yaml x-speakeasy-retries: statusCodes: 408,504 strategy: backoff ``` Note that the `Meta` function had to use JSON syntax in the value to output an extension object with sub-properties. ## Create SDKs With Speakeasy Before continuing with this tutorial, please register at https://app.speakeasy.com. Once you've registered, create a workspace named `club`. Browse to API keys. Click "New Api Key". Name it `club`. Copy and save the key content to use later. ### Set Up the Speakeasy CLI The CLI is the simplest way to use Speakeasy. This tutorial uses Docker, but if you want to install Speakeasy directly on your computer in the future, follow the instructions in the [readme](https://github.com/speakeasy-api/speakeasy#installation). Run the commands below in the second terminal to `gobox` that you used to log in to a new Docker terminal. Use the API key you saved earlier in the last line. ```bash apt update; apt install -y curl unzip sudo nodejs npm; # install dependencies curl -fsSL https://go.speakeasy.com/cli-install.sh | sh; # install Speakeasy export SPEAKEASY_API_KEY=your_api_key_here; # <-- overwrite this with your key ``` Now Speakeasy is installed in the container. Test it by running: ```bash speakeasy help; ``` ### Build an SDK You now have `app/gen/http/openapi3.yaml` and Speakeasy, so you can build the SDK. In the container terminal, run: ```bash speakeasy quickstart ``` The output should be: ```bash Authenticated with workspace successfully - https://app.speakeasy.com/workspaces/ Generating SDK for typescript... INFO operation valid {"operation":"band#/openapi.json","type":"paths"} INFO operation valid {"operation":"band#play","type":"paths"} INFO operation valid {"operation":"order#tea","type":"paths"} Generating SDK for typescript... done ✓. For more docs on customizing the SDK check out: https://www.speakeasy.com/docs/customize-sdks ``` Remember to give yourself permissions to the files on your host machine in another terminal: ```bash sudo chown -R $(id -u):$(id -g) . ``` While you're using only TypeScript in this tutorial, Speakeasy supports [C#, Go, Java, PHP, Python, Ruby, and TypeScript](/docs/sdks/create-client-sdks#language-support). #### Explore the Generated Files The root of the Speakeasy-generated `sdk` folder contains various npm files. Importantly, the `package.json` file lists the dependencies you need to install before you can run the SDK. The `docs` folder contains your SDK documentation in Markdown files. The `src` folder contains the SDK code. ### Call Your Service With the SDK We'll implement the documentation example in `sdk/docs/sdks/drinkoperations/README.md`. Make a file called `test.js` in the `app/sdk` folder. Insert the code below. ```js const SDK = require("./dist/index"); (async () => { const sdk = new SDK.SDK(); const res = await sdk.drinkOperations.orderNumberTea({ includeMilk: true, isGreen: false, numberSugars: 1584355970564842800, }); if (res.statusCode == 200) { console.log("A nice cup of tea"); } })(); ``` In the container terminal, install the npm dependencies and run the test file. ```bash cd /go/src/app/sdk; npm install; npx tsc --build; node test.js; ``` In the first terminal where your `club` server is still running, you should see that the server has received a request. In the second terminal, you should see it receive `A nice cup of tea`. ## Next Steps You now know how to make an OpenAPI-compliant web service from a Goa design specification, and how to generate SDKs for it using Speakeasy. ### Get Help With Advanced Goa If you want to build a more complex API and need help understanding Goa, read the full [design language specification](https://pkg.go.dev/goa.design/goa/dsl). You can also use the Go Slack group to ask for help: - Register for the group at https://invite.slack.golangbridge.org. - Log in at https://gophers.slack.com. - Join the Goa channel to ask questions about the framework. Perhaps the easiest way to find out how to do something (especially when using `Meta`) is to search the test cases when you have cloned the [source code](https://github.com/goadesign/goa). ### Use Speakeasy Customizations Review the [Speakeasy customizations](https://www.speakeasy.com/docs/customize-sdks) to see if adding any would make your service more understandable or usable. # How to generate an OpenAPI/Swagger spec with gRPC Gateway Source: https://speakeasy.com/openapi/frameworks/grpc-gateway import { Callout } from "@/mdx/components"; You may want to provide a RESTful API in addition to your gRPC service without the need to duplicate your code. [gRPC Gateway](https://grpc-ecosystem.github.io/grpc-gateway/) is a popular tool for generating RESTful APIs from gRPC service definitions. In this tutorial, we'll take a detailed look at how to use gRPC Gateway to generate an OpenAPI schema based on a Protocol Buffers (protobuf) gRPC service definition. Afterward, we can use Speakeasy to read our generated OpenAPI schema and create a production-ready SDK. If you want to follow along, you can use the [**gRPC Speakeasy Bar example repository**](https://github.com/speakeasy-api/speakeasy-grpc-gateway-example). ## An Overview of gRPC Gateway [gRPC Gateway](https://grpc-ecosystem.github.io/grpc-gateway/) is a [protoc](https://github.com/protocolbuffers/protobuf) plugin that reads gRPC service definitions and generates a reverse proxy server that translates a RESTful JSON API into gRPC. This way, you can expose an HTTP endpoint that can be called by clients that don't support gRPC. The generated server code will forward incoming JSON requests to your gRPC server and translate the responses to JSON. gRPC Gateway also generates an OpenAPI schema that describes your API. You can use this schema to create SDKs for your API. ## OpenAPI Versions gRPC Gateway outputs OpenAPI 2.0, and Speakeasy supports OpenAPI 3.0 and 3.1. To generate an OpenAPI 3.0 or 3.1 schema, you'll need to convert the OpenAPI 2.0 schema to at least OpenAPI 3.0. ## The Protobuf to REST SDK Pipeline To generate a REST API with a developer-friendly SDK, we'll follow these three core steps: 1. **gRPC to OpenAPI:** First, we will use gRPC Gateway to produce an OpenAPI schema based on our protobuf service definition. This generated schema is in OpenAPI 2.0 format. 2. **OpenAPI 2.0 to OpenAPI 3.x:** Next, as gRPC Gateway's output schema is in OpenAPI 2.0 and we need at least OpenAPI 3.0 for our SDK, we will convert the generated schema from OpenAPI 2.0 to OpenAPI 3.0. 3. **OpenAPI 3.x to SDK:** Finally, once we have the OpenAPI 3.0 schema, we will leverage Speakeasy to create our SDK based on the OpenAPI 3.0 schema derived from the previous steps. By following these steps, we can ensure we have a robust, production-ready SDK that adheres to our API's specifications. ## Step-by-Step Tutorial: From Protobuf to OpenAPI to an SDK Now let's walk through generating an OpenAPI schema and SDK for our Speakeasy Bar gRPC service. ### Check Out the Example Repository If you would like to follow along, start by cloning the example repository: ```bash filename="Terminal" git clone git@github.com:speakeasy-api/speakeasy-grpc-gateway-example.git cd speakeasy-grpc-gateway-example ``` ### Install Go To generate an OpenAPI schema from a protobuf file, we'll need to install Go and protoc. This tutorial was written using Go 1.21.4. On macOS, install Go by running: ```bash filename="Terminal" brew install go ``` Alternatively, follow the [Go installation instructions](https://go.dev/doc/install) for your platform. ### Install Buf We'll use the [Buf CLI](https://buf.build/) as an alternative to protoc so that we can save our generation configuration as YAML. Buf is compatible with protoc plugins. On macOS, install Buf by running: ```bash filename="Terminal" brew install bufbuild/buf/buf ``` Alternatively, follow the [Buf CLI installation instructions](https://buf.build/docs/installation) for your platform. ### Install Buf Modules We'll use Buf modules to manage our dependencies. ```bash filename="Terminal" cd proto buf mod update cd .. ``` ### Install protoc-gen-go Buf requires the `protoc-gen-go` plugin to generate Go code from protobuf files. Install `protoc-gen-go` by running: ```bash filename="Terminal" go install google.golang.org/protobuf/cmd/protoc-gen-go@latest ``` Make sure that the `protoc-gen-go` binary is in your `$PATH`. On macOS, you can achieve that by running the following command if the `go/bin` directory is not already in your path. ```bash filename="Terminal" export PATH=${PATH}:`go env GOPATH`/bin ``` ### Install Go Requirements ```bash filename="Terminal" go mod tidy go install \ github.com/grpc-ecosystem/grpc-gateway/v2/protoc-gen-grpc-gateway \ github.com/grpc-ecosystem/grpc-gateway/v2/protoc-gen-openapiv2 \ google.golang.org/protobuf/cmd/protoc-gen-go \ google.golang.org/grpc/cmd/protoc-gen-go-grpc ``` ### Generate the Go Code We'll use Buf to generate the Go code from the protobuf file. Run the following in the terminal: ```bash filename="Terminal" buf generate ``` Buf reads the configuration in `buf.gen.yaml`, then generates the Go code in the `proto` directory. This will generate the `proto/speakeasy/v1/speakeasy.pb.go`, `proto/speakeasy/v1/speakeasy_grpc.pb.go`, and `proto/speakeasy/v1/speakeasy.pb.gw.go` files. ### Generate the OpenAPI Schema Because we have the `openapiv2` protoc plugin configured in our `buf.gen.yaml` file, Buf will generate an OpenAPI schema and save it as `openapi/speakeasy/v1/speakeasy.swagger.json`. This is the OpenAPI 2.0 schema that gRPC Gateway generates by default. ### Convert the OpenAPI Schema to OpenAPI 3.0 We'll use the excellent [kin-openapi](https://github.com/getkin/kin-openapi) Go library to convert the OpenAPI 2.0 schema to OpenAPI 3.0. In `convert/convert.go`, we use `kin-openapi` to unmarshal `openapi/speakeasy/v1/speakeasy.swagger.json`, then convert it to OpenAPI 3.0, then marshal it back to JSON, and finally write it to `openapi/speakeasy/v1/speakeasy.openapi.json`. To do the conversion, run the following in the terminal: ```bash filename="Terminal" go run convert/convert.go ``` ## How To Customize the API Schema By modifying the protobuf service definition, we can customize the generated OpenAPI schema. We'll start with a basic example and add options to enhance the schema. ## Understanding the Speakeasy Bar Protobuf Service Let's explore the Speakeasy Bar protobuf service definition in `proto/speakeasy/v1/speakeasy.proto`. This is the starting point for our gRPC service: ```cpp filename="proto/speakeasy/v1/speakeasy.proto" syntax = "proto3"; package speakeasy.v1; message Drink { string product_code = 1; string name = 2; string description = 3; double price = 4; } message GetDrinkRequest { string product_code = 1; } message GetDrinkResponse { Drink drink = 1; } message ListDrinksRequest { // Empty request } message ListDrinksResponse { repeated Drink drinks = 1; } service SpeakeasyService { rpc GetDrink(GetDrinkRequest) returns (GetDrinkResponse) { option (google.api.http) = { get: "/v1/drinks/{product_code}" }; } rpc ListDrinks(ListDrinksRequest) returns (ListDrinksResponse) { option (google.api.http) = { get: "/v1/drinks" }; } } ``` The service defines one object type called `Drink` with properties like `product_code`, `name`, `description`, and `price`. It also defines a service called `SpeakeasyService` that has two methods: 1. `GetDrink`: Retrieves a single drink by its product code 2. `ListDrinks`: Lists all available drinks ### Adding API Information to the Service We can enhance our protobuf definition by adding information about the API to improve the generated OpenAPI schema. We'll use the `options.openapiv2_swagger` option from `grpc.gateway.protoc_gen_openapiv2`: ```cpp filename="proto/speakeasy/v1/speakeasy.proto" option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_swagger) = { info: { title: "Speakeasy Bar API" description: "An API for the Speakeasy Bar, featuring a variety of cocktails and drinks." version: "0.1.0" } host: "api.speakeasybar.com" schemes: HTTP schemes: HTTPS consumes: "application/json" produces: "application/json" tags: [ { name: "drinks" description: "Operations related to drinks and cocktails offered by the Speakeasy Bar." } ] }; ``` This adds several important pieces of information to our API: 1. **Title, Description, and Version**: Basic information about our API that appears in the `info` object of the generated OpenAPI schema: ```json filename="openapi/speakeasy/v1/speakeasy.openapi.json" { "openapi": "3.0.0", "info": { "title": "Speakeasy Bar API", "description": "An API for the Speakeasy Bar, featuring a variety of cocktails and drinks.", "version": "0.1.0" }, ... } ``` 2. **Server Information**: We add a server to the API using the `host` key. Our conversion script will add the `servers` object to the generated OpenAPI schema: ```json filename="openapi/speakeasy/v1/speakeasy.openapi.json" { "servers": [ { "url": "https://api.speakeasybar.com" } ] } ``` ### Adding Descriptions and Examples to Components To create an SDK that offers a great developer experience, we recommend adding descriptions and examples to all fields in OpenAPI components. We'll enhance the `Drink` object type: ```cpp filename="proto/speakeasy/v1/speakeasy.proto" message Drink { option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_schema) = { json_schema: { title: "Drink" description: "A drink available at the Speakeasy Bar." example: "{\"product_code\":\"602a7da9-b8bb-46e6-b288-457b561029b8\",\"name\":\"Old Fashioned\",\"description\":\"A classic cocktail made with whiskey, sugar, bitters, and a twist of citrus.\",\"price\":12.99}" } }; string product_code = 1 [(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { description: "The unique identifier for the drink." pattern: "^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$" format: "uuid" example: "\"602a7da9-b8bb-46e6-b288-457b561029b8\"" }]; string name = 2 [(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { description: "The name of the drink." example: "\"Old Fashioned\"" min_length: 1 }]; string description = 3 [(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { description: "A description of the drink." example: "\"A classic cocktail made with whiskey, sugar, bitters, and a twist of citrus.\"" }]; double price = 4 [(grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { description: "The price of the drink in USD." example: "12.99" minimum: 0 }]; } ``` In this enhanced definition: 1. We add a `title`, `description`, and `example` to the `Drink` object type. The `example` is a stringified JSON object. 2. We use `openapiv2_field` to add options to the fields in the `Drink` object type. For example, we add a `description`, `pattern`, `format`, and `example` to the `productCode` field: ```json filename="openapi/speakeasy/v1/speakeasy.openapi.json" { "productCode": { "type": "string", "description": "The unique identifier for the drink.", "pattern": "^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$", "format": "uuid", "example": "602a7da9-b8bb-46e6-b288-457b561029b8" } } ``` If you use Speakeasy to create an SDK, this description and example will appear in the generated documentation and usage examples. For instance, in the TypeScript SDK's documentation: ```typescript filename="sdk/docs/sdks/drinks/README.md" import { SDK } from "openapi"; (async() => { const sdk = new SDK(); const res = await sdk.drinks.getDrink({ productCode: "602a7da9-b8bb-46e6-b288-457b561029b8", }); if (res.statusCode == 200) { // handle response } })(); ``` Note how the `productCode` field is represented by our UUID example instead of a random string. ### Customizing the OperationId By default, the `operationId` is the method name in the protobuf service definition. We can customize the `operationId` for each method using `options.openapiv2_operation`: ```cpp filename="proto/speakeasy/v1/speakeasy.proto" rpc GetDrink(GetDrinkRequest) returns (GetDrinkResponse) { option (google.api.http) = { get: "/v1/drinks/{product_code}" }; option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { operation_id: "getDrink" // More options... }; } ``` This appears in the generated OpenAPI schema: ```json filename="openapi/speakeasy/v1/speakeasy.openapi.json" { "operationId": "getDrink" } ``` ### Adding Descriptions and Tags to Methods We can also add descriptions and tags to methods using `options.openapiv2_operation`: ```cpp filename="proto/speakeasy/v1/speakeasy.proto" rpc GetDrink(GetDrinkRequest) returns (GetDrinkResponse) { option (google.api.http) = { get: "/v1/drinks/{product_code}" }; option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { operation_id: "getDrink" summary: "Get a drink" description: "Get a drink by its product code." tags: "drinks" // More options... }; } ``` This adds tags and descriptions to the operation in the OpenAPI schema: ```json filename="openapi/speakeasy/v1/speakeasy.openapi.json" { "tags": [ "drinks" ], "summary": "Get a drink", "description": "Get a drink by its product code." } ``` ### Adding Tag Descriptions We can add descriptions to tags in the protobuf definition by using `options.openapiv2_swagger`: ```cpp filename="proto/speakeasy/v1/speakeasy.proto" option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_swagger) = { // Other options... tags: [ { name: "drinks" description: "Operations related to drinks and cocktails offered by the Speakeasy Bar." } ] }; ``` This adds descriptions to tags in the OpenAPI schema: ```json filename="openapi/speakeasy/v1/speakeasy.openapi.json" { "tags": [ { "name": "drinks", "description": "Operations related to drinks and cocktails offered by the Speakeasy Bar." } ] } ``` ### Adding OpenAPI Extensions gRPC Gateway allows us to add OpenAPI extensions to the schema using the `extensions` field in our protobuf service definition. For example, we can add the [Speakeasy retries extension](/docs/customize-sdks/retries) called `x-speakeasy-retries`, which causes the SDK to retry failed requests: ```cpp filename="proto/speakeasy/v1/speakeasy.proto" rpc GetDrink(GetDrinkRequest) returns (GetDrinkResponse) { option (google.api.http) = { get: "/v1/drinks/{product_code}" }; option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { // Other options... extensions: { key: "x-speakeasy-retries" value: { string_value: "{\"strategy\":\"backoff\",\"backoff\":{\"initialInterval\":500,\"maxInterval\":60000,\"maxElapsedTime\":3600000,\"exponent\":1.5},\"statusCodes\":[\"5XX\"],\"retryConnectionErrors\":true}" } } }; } ``` This adds the extension to the OpenAPI schema: ```json filename="openapi/speakeasy/v1/speakeasy.openapi.json" { "x-speakeasy-retries": { "strategy": "backoff", "backoff": { "initialInterval": 500, "maxInterval": 60000, "maxElapsedTime": 3600000, "exponent": 1.5 }, "statusCodes": [ "5XX" ], "retryConnectionErrors": true } } ``` ## Create an SDK With Speakeasy Now that we have an OpenAPI 3.0 schema, we can create an SDK with Speakeasy. Speakeasy will create documentation and usage examples based on the descriptions and examples we added. We'll use the `speakeasy quickstart` command to create an SDK for the Speakeasy Bar gRPC service. Run the following in the terminal: ```bash filename="Terminal" speakeasy quickstart ``` Follow the onscreen prompts to provide the necessary configuration details for your new SDK such as the name, schema location and output path. Enter `openapi/speakeasy/v1/speakeasy.openapi.json` when prompted for the OpenAPI document location and select TypeScript when prompted for which language you would like to generate. ## Example Protobuf Definition and SDK Generator The source code for our complete example is available in the [**gRPC Speakeasy Bar example repository**](https://github.com/speakeasy-api/speakeasy-grpc-gateway-example). The repository contains a TypeScript SDK and instructions on how to create more SDKs. You can clone this repository to test how changes to the protobuf definition result in changes to the SDK. After modifying your protobuf definition, you can run the following in the terminal to create a new SDK: ```bash filename="Terminal" buf generate && go run convert/convert.go && speakeasy quickstart ``` Happy generating! # How to generate an OpenAPI document with Hono Source: https://speakeasy.com/openapi/frameworks/hono import { Callout } from "@/mdx/components"; This guide walks you through generating an OpenAPI document for a [Hono](https://hono.dev/) API and using Speakeasy to create an SDK based on the generated document. Here's what we'll do: 1. Add Zod OpenAPI and Scalar UI to a Node.js Hono project. 2. Generate an OpenAPI document using the Zod OpenAPI Hono middleware. 3. Improve the generated OpenAPI document to prepare it for code generation. 4. Use the Speakeasy CLI to generate an SDK based on the OpenAPI document. 5. Add Speakeasy OpenAPI extensions to improve the generated SDK. We'll also take a look at how you can use the generated SDK. Your Hono project might not be as simple as our example app, but the steps below should translate well to any Hono project. ## The OpenAPI creation pipeline [Zod OpenAPI](https://hono.dev/examples/zod-openapi#zod-openapi) is Hono middleware that allows you to validate values and types using [Zod](https://zod.dev/) and generate an OpenAPI document. We'll begin by defining data schemas with Zod, then set up the Hono app to generate an OpenAPI document. The quality of your OpenAPI document determines the quality of generated SDKs and documentation, so we'll look into ways you can improve the generated document based on the Speakeasy [OpenAPI best practices](/docs/prep-openapi/best-practices). We'll then use the improved OpenAPI document to generate an SDK using Speakeasy. We'll explain how to add SDK creation to a CI/CD pipeline so that Speakeasy automatically generates fresh SDKs whenever your Hono API changes in the future. Finally, we'll use a simplified example to demonstrate how to use the generated SDK. ## Requirements This guide assumes that you have an existing Hono app and basic familiarity with Hono. The source code for the completed example is available in the [Speakeasy Hono example repository](https://github.com/speakeasy-api/examples/tree/main/framework-hono-ts). If you want to follow along step-by-step, you can clone the [Speakeasy Hono example repository](https://github.com/speakeasy-api/examples/tree/main/frameworks-hono-ts/initial-app), which has the initial state of the app that we'll use to start this tutorial within the `initial-app` directory. Ensure the following are installed on your machine: - [Node.js version 18 or above](https://nodejs.org/en/download) (we used Node v22.15.1), which the Hono Node.js Adapter requires. - The [Speakeasy CLI](/docs/speakeasy-cli/getting-started), which we'll use to generate an SDK from the OpenAPI document. If you're using the [example initial application](https://github.com/speakeasy-api/examples/tree/main/frameworks-hono-ts/initial-app), add a `.env` file containing the following environment variables to the root of the initial app project: ```env NODE_ENV=development PORT=3000 ``` ## Adding the Zod OpenAPI middleware to a Hono project We'll use the [Zod OpenAPI Hono](https://hono.dev/examples/zod-openapi) middleware to generate an OpenAPI document. We'll create Zod schemas to validate values and types and to generate part of the OpenAPI document. ### Creating Zod schemas First, install the middleware and Zod: ```bash filename="Terminal" npm i zod @hono/zod-openapi ``` Next, create a `schemas.ts` file in the `src` folder and create Zod schemas for your data: ```typescript filename="schema.ts" import { z } from "@hono/zod-openapi"; export const UserSelectSchema = z.object({ id: z.string(), name: z.string(), age: z.number(), }); export const UserInsertSchema = z.object({ name: z.string(), age: z.number(), }); export const patchUserSchema = UserInsertSchema.partial(); ``` The `z` object should be imported from `@hono/zod-openapi`. Note that Hono also has a [Standard Schema validator](https://github.com/honojs/middleware/tree/main/packages/standard-validator) that lets you write schemas and validate the incoming values using the same interface for multiple validation libraries that support Standard Schema. Supported validation libraries include Zod, Valibot, and ArkType. Create schemas for your request-query parameters, messages, and error responses: ```typescript filename="schema.ts" export const idParamsSchema = z.object({ id: z.string().min(3), }); export function createMessageObjectSchema( exampleMessage: string = "Hello World", ) { return z.object({ message: z.string(), }); } export function createErrorSchema(schema: T) { const { error } = schema.safeParse( schema._def.typeName === z.ZodFirstPartyTypeKind.ZodArray ? [] : {}, ); return z.object({ success: z.boolean(), error: z.object({ issues: z.array( z.object({ code: z.string(), path: z.array(z.union([z.string(), z.number()])), message: z.string().optional(), }), ), name: z.string(), }), }); } ``` Create a `types.ts` file in the `src/lib` folder and add the `ZodSchema` type to it: ```typescript filename="types.ts" import type { z } from "@hono/zod-openapi"; // eslint-disable-next-line ts/ban-ts-comment // @ts-expect-error export type ZodSchema = | z.ZodUnion | z.AnyZodObject | z.ZodArray; ``` Import this type in the `src/schemas.ts` file. ```typescript filename="schemas.ts" import type { ZodSchema } from "./lib/types"; ``` ### Replacing the Hono instances with OpenAPIHono Set up your app to use the `OpenAPIHono` instance of the Zod OpenAPI middleware instead of the `Hono` instance. Import the `OpenAPIHono` class in the `src/lib/createApp.ts` file: ```typescript filename="createApp.ts" import { OpenAPIHono } from "@hono/zod-openapi"; ``` Remove the `Hono` import and replace the `Hono` instances with `OpenAPIHono`: ```diff filename="createApp.ts" - return new Hono({ strict: false }); + return new OpenAPIHono({ strict: false }); ``` ```diff filename="createApp.ts" - const app = new Hono({ strict: false }); + const app = new OpenAPIHono({ strict: false }); ``` The `OpenAPIHono` class is an extension of the `Hono` class that gives `OpenAPIHono` its OpenAPI document-generation functionality. ### Defining routes Let's split the routes and handlers into separate files for better code organization. Create a `users.routes.ts` file in the `src/routes/users` folder and use the Zod OpenAPI `createRoute` method to define your routes: ```typescript filename="users.routes.ts" import { createErrorSchema, createMessageObjectSchema, idParamsSchema, patchUserSchema, UserInsertSchema, UserSelectSchema, } from "@/schemas"; import { createRoute, z } from "@hono/zod-openapi"; export const list = createRoute({ path: "/users", method: "get", responses: { 200: { content: { "application/json": { schema: z.array(UserSelectSchema), }, }, description: "The list of users", }, }, }); export const create = createRoute({ path: "/users", method: "post", request: { body: { content: { "application/json": { schema: UserInsertSchema, }, }, description: "The user to create", required: true, }, }, responses: { 200: { content: { "application/json": { schema: UserSelectSchema, }, }, description: "The created user", }, 404: { content: { "application/json": { schema: createMessageObjectSchema("Not Found"), }, }, description: "User not found", }, 422: { content: { "application/json": { schema: createErrorSchema(patchUserSchema), }, }, description: "The validation error(s)", }, }, }); export const getOne = createRoute({ path: "/users/{id}", method: "get", request: { params: idParamsSchema, }, responses: { 200: { content: { "application/json": { schema: UserSelectSchema, }, }, description: "The requested user", }, 404: { content: { "application/json": { schema: createMessageObjectSchema("Not Found"), }, }, description: "User not found", }, 422: { content: { "application/json": { schema: createErrorSchema(patchUserSchema), }, }, description: "Invalid id error", }, }, }); export type ListRoute = typeof list; export type CreateRoute = typeof create; export type GetOneRoute = typeof getOne; ``` The `createRoute` function takes in an object that describes the route's request and possible responses. The Zod schema defines the request and response content. The route types are then exported for use in the route handlers. ### Defining route handlers Create a `users.handlers.ts` file in the `src/routes/users` folder and add the following route handlers to it: ```typescript filename="users.handlers.ts" import type { AppRouteHandler } from "@/lib/types"; import type { CreateRoute, GetOneRoute, ListRoute, } from "@/routes/users/users.routes"; export const list: AppRouteHandler = async (c) => { // TODO: db query to get all users return c.json( [ { age: 42, id: "123", name: "John Doe", }, { age: 32, id: "124", name: "Sarah Jones", }, ], 200, ); }; export const create: AppRouteHandler = async (c) => { const user = c.req.valid("json"); console.log({ user }); // TODO: db query to create a user return c.json( { id: "2342", age: user.age, name: user.name, }, 200, ); }; export const getOne: AppRouteHandler = async (c) => { const { id } = c.req.valid("param"); // TODO: db query to get a user by id const foundUser = { age: 50, id, name: "Lisa Smith", }; if (!foundUser) { return c.json( { message: "Not found", }, 404, ); } return c.json(foundUser, 200); }; ``` Add the following `AppRouteHandler` type to the `src/lib/types.ts` file: ```typescript filename="types.ts" import type { RouteConfig, RouteHandler } from "@hono/zod-openapi"; export type AppRouteHandler = RouteHandler; ``` The handlers are made type safe by the route types. The request and response data in the Hono [context object](https://hono.dev/docs/api/context) is type checked using the schema defined in the routes. If you use an incorrect type, for example, by setting `age:` to `42`, you'll get a type error. ### Configuring the middleware for each endpoint Replace the code in the `src/routes/users/users.index.ts` file with the following lines of code: ```typescript filename="users.index.ts" import { createRouter } from "@/lib/createApp"; import * as handlers from "./users.handlers"; import * as routes from "./users.routes"; const router = createRouter() .openapi(routes.list, handlers.list) .openapi(routes.create, handlers.create) .openapi(routes.getOne, handlers.getOne); export default router; ``` The `openapi` method takes the route and the handler as its arguments and configures the Zod OpenAPI middleware for each endpoint on the `OpenAPIHono` instance. ## Configuring and generating the OpenAPI document Create a file called `configureOpenAPI.ts` in the `src/lib` folder and add the following lines of code to it: ```typescript filename="configureOpenAPI.ts" import type { OpenAPIHono } from "@hono/zod-openapi"; import packageJson from "../../package.json"; export const openAPIObjectConfig = { openapi: "3.1.0", externalDocs: { description: "Find out more about Users API", url: "www.example.com", }, info: { version: packageJson.version, title: "Users API", }, }; export default function configureOpenAPI(app: OpenAPIHono) { app.doc31("/doc", openAPIObjectConfig); } ``` The `configureOpenAPI` function takes in an `OpenAPIHono` instance and uses the `doc31` method to generate an OpenAPI document based on the OpenAPI Specification version 3.1. We can view this document at the `'/doc'` endpoint. We then pass in the OpenAPI configuration object to the function to add fields to the root object of the OpenAPI document. Now, pass in the `OpenAPIHono` app instance to the `configureOpenAPI` function in the `src/app.ts` file: ```typescript filename="app.ts" import configureOpenAPI from "./lib/configureOpenAPI"; configureOpenAPI(app); ``` ## Supported OpenAPI Specification versions in Hono and Speakeasy Speakeasy currently supports the OpenAPI Specification versions 3.0.x and 3.1.x. We recommend using OpenAPI Specification version 3.1 if possible, as it's fully compatible with [JSON Schema](https://json-schema.org/), which gives you access to a large ecosystem of tools and libraries. Zod OpenAPI Hono can generate an OpenAPI document using version 3.0 or version 3.1 of the OpenAPI Specification. This guide uses version 3.1. Run the development server `npm run dev` and open [`http://localhost:3000/doc`](http://localhost:3000/doc) to see the OpenAPI document in JSON format: ```json { "openapi": "3.1.0", "externalDocs": { "description": "Find out more about Users API", "url": "www.example.com" }, "info": { "version": "1.0.0", "title": "Users API" }, "components": { "schemas": { }, "parameters": { } }, "paths": { "/users": { "get": { "responses": { "200": { "description": "The list of users", "content": { "application/json": { "schema": { "type": "array", "items": { "type": "object", ... ``` ## Adding Scalar UI middleware Let's use the [Scalar UI middleware](https://www.npmjs.com/package/@scalar/hono-api-reference) to add an interactive documentation UI for the API. Install the middleware: ```bash filename="Terminal" npm i @scalar/hono-api-reference ``` Import the `Scalar` middleware in the `src/lib/configureOpenAPI.ts` file: ```typescript filename="configureOpenAPI.ts" import { Scalar } from "@scalar/hono-api-reference"; ``` Add `Scalar` as a handler for GET requests to the `/ui` route: ```typescript filename="configureOpenAPI.ts" // !mark(3) export default function configureOpenAPI(app: OpenAPIHono) { (app.doc31("/doc", openAPIObjectConfig), app.get( "/ui", Scalar({ url: "/doc", pageTitle: "Users Management API", }), )); } ``` Open your browser and navigate to [`http://localhost:3000/ui`](http://localhost:3000/ui). You should see the Scalar UI with three API endpoints in the sidebar: ![Scalar UI endpoints](/assets/openapi/hono/scalar-ui.png) You can see the parameters required for API endpoints and try out the different API endpoints. In the `http://localhost:3000/doc` route, you can also view the OpenAPI document in JSON format. ## Registering the Zod schemas as reusable OpenAPI component schemas The request and response content schemas of the OpenAPI document are inline: ```json "components": { "schemas": {}, "parameters": {} }, "paths": { "/users": { "get": { "responses": { "200": { "description": "The list of users", "content": { "application/json": { "schema": { "type": "array", "items": { "type": "object", "properties": { "id": { "type": "string" }, ``` Let's make these schemas reusable by adding them to the OpenAPI [Components Object](https://swagger.io/specification/#components-object). Use the [`.openapi()`](https://github.com/asteasolutions/zod-to-openapi#the-openapi-method) method on the Zod object to register your Zod schemas as referenced components in the `src/schemas.ts` file: ```typescript filename="schemas.ts" // !mark(7) export const UserSelectSchema = z .object({ id: z.string(), name: z.string(), age: z.number(), }) .openapi("UserSelect"); ``` This adds your schemas to the OpenAPI components object: ```json "components": { "schemas": { "UserSelect": { "type": "object", "properties": { "id": { "type": "string" }, "name": { "type": "string" }, "age": { "type": "number" } }, "required": [ "id", "name", "age" ] }, ``` The schemas are referenced using a [Reference Object](https://swagger.io/specification/#reference-object) (`$ref`), which is a reference identifier that specifies the location (as a URI) of the value being referenced. ```json "responses": { "200": { "description": "The created user", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/UserSelect" } } } } } ``` ## Adding OpenAPI metadata to the Zod schemas Let's add additional OpenAPI metadata to our schemas. In the `src/schemas.ts` file, add example values by passing in an object with an `example` property to the `openapi` method: ```typescript filename="schemas.ts" // !mark(4:6) export const UserSelectSchema = z .object({ id: z.string().openapi({ example: "123", }), // !mark(2:4) name: z.string().openapi({ example: "John Doe", }), // !mark(2:4) age: z.number().openapi({ example: 42, }), }) .openapi("UserSelect"); // !mark(4:6) export const UserInsertSchema = z .object({ name: z.string().openapi({ example: "John Doe", }), age: z .number() // !mark(1:3) .openapi({ example: 42, }), }) .openapi("UserInsert"); ``` Define the route parameters for parameter schema: ```typescript filename="schemas.ts" // !mark(6:9) export const idParamsSchema = z.object({ id: z .string() .min(3) .openapi({ param: { name: "id", in: "path", }, example: "423", }) .openapi("idParams"), }); ``` After adding the OpenAPI metadata to your schemas, you'll see that your OpenAPI document and Scalar UI will show example values for the schemas used in requests and responses: ![Scalar UI POST request example values](/assets/openapi/hono/scalar-ui-example-post.png) You can also view the details of the example data schemas: ![Scalar UI example data schema](/assets/openapi/hono/scalar-ui-data-schema.png) ## Adding the OpenAPI operationId using Hono Zod OpenAPI In the OpenAPI document, each HTTP request has an `operationId` that identifies the operation. The `operationId` is also used to generate method names and documentation in SDKs. To add an `operationId`, use the `operationId` property of the `createRoute` method in the `src/routes/users/users.routes.ts` file: ```typescript filename="users.routes.ts" export const list = createRoute({ operationId: 'getUsers', ``` ## Adding OpenAPI tags to Zod OpenAPI Hono routes Whether you're building a big application or only have a handful of operations, we recommend adding tags to all your Hono routes so you can group them by tag in generated SDK code and documentation. ### Adding OpenAPI tags to routes in Hono To add an OpenAPI tag to a Zod OpenAPI Hono route, use the `tags` property to pass in an array of tags as the argument object of the `createRoute` method call: ```typescript filename="users.routes.ts" tags: ['Users'], ``` ### Adding metadata to tags We can add metadata to the tag by passing in a [Tag Object](https://swagger.io/specification/#tag-object), instead of a string, to a tag array item. Add a tag to the root OpenAPI object `openAPIObjectConfig` in the `src/lib/configureOpenAPI.ts` file: ```typescript filename="configureOpenAPI.ts" // !mark(7:14) export const openAPIObjectConfig = { openapi: '3.1.0', externalDocs: { description: 'Find out more about Users API', url: 'https://www.example.com', }, tags: [{ name: 'Users', description: 'Users operations', externalDocs: { description: 'Find more info here', url: 'https://example.com', }, }], ``` ## Adding a list of servers to the Hono OpenAPI document When validating an OpenAPI document, Speakeasy expects a list of servers at the root of the document. Add a server by adding a `servers` property to the `openAPIObjectConfig` object: ```typescript filename="configureOpenAPI.ts" // !mark(7:12) export const openAPIObjectConfig = { openapi: '3.1.0', externalDocs: { description: 'Find out more about Users API', url: 'https://www.example.com', }, servers: [ { url: 'http://localhost:3000/', description: 'Development server', }, ], ``` ## Adding retries to your SDK with x-speakeasy-retries [OpenAPI document extensions](/openapi/extensions) allow us to add vendor-specific functionality to the document. - Extension fields must be prefixed with `x-`. - Speakeasy uses extensions that start with `x-speakeasy-`. Let's add a [Speakeasy extension](/docs/speakeasy-reference/extensions) that adds retries to requests from Speakeasy SDKs by adding a top-level `x-speakeasy-retries` schema to the OpenAPI document. We can also override the retry strategy per operation. ### Adding global retries Apply the Speakeasy retries extension globally by adding the following `'x-speakeasy-retries'` property to the `openAPIObjectConfig` object: ```typescript filename="configureOpenAPI.ts" // !mark(13:23) export const openAPIObjectConfig = { openapi: '3.1.0', externalDocs: { description: 'Find out more about Users API', url: 'https://www.example.com', }, servers: [ { url: 'http://localhost:3000/', description: 'Development server', }, ], 'x-speakeasy-retries': { strategy: 'backoff', backoff: { initialInterval: 500, maxInterval: 60000, maxElapsedTime: 3600000, exponent: 1.5, }, statusCodes: ['5XX'], retryConnectionErrors: true, }, ``` ### Adding retries per method To create a unique retry strategy for a single route, add an `'x-speakeasy-retries'` property to the `createRoute` method call's argument object in the `src/routes/users/users.routes.ts` file: ```typescript filename="users.routes.ts" // !mark(6:16) export const list = createRoute({ 'operationId': 'getUsers', 'path': '/users', 'method': 'get', 'tags': ['Users'], 'x-speakeasy-retries': { strategy: 'backoff', backoff: { initialInterval: 300, maxInterval: 40000, maxElapsedTime: 3000000, exponent: 1.2, }, statusCodes: ['5XX'], retryConnectionErrors: true, }, ``` ## Generating an SDK based on your OpenAPI document Before generating an SDK, we need to save the Hono Zod OpenAPI-generated OpenAPI document to a file. OpenAPI files are written as JSON or YAML; we'll save it as a YAML file, as it's easier to read. ### Saving the OpenAPI document to a YAML file using a Node.js script First install the [JS-YAML](https://www.npmjs.com/package/js-yaml) package: ```bash filename="Terminal" npm i js-yaml ``` Then install the types for the package: ```bash filename="Terminal" npm i --save-dev @types/js-yaml ``` Now let's create a script to convert the OpenAPI object to a YAML file. We'll use the JS-YAML library to convert the OpenAPI object to a YAML string. Create a script called `generateOpenAPIYamlFile.ts` in the `src` folder and add the following lines of code to it: ```typescript filename="generateOpenAPIYamlFile.ts" import { writeFileSync } from "node:fs"; import users from "@/routes/users/users.index"; import * as yaml from "js-yaml"; import configureOpenAPI, { openAPIObjectConfig } from "./lib/configureOpenAPI"; import createApp from "./lib/createApp"; const app = createApp(); const routes = [users] as const; configureOpenAPI(app); routes.forEach((route) => { app.route("/", route); }); // Convert the OpenAPIObject to a YAML string const yamlString = yaml.dump(app.getOpenAPI31Document(openAPIObjectConfig)); // Save the YAML string to a file writeFileSync("openapi.yaml", yamlString); ``` This initializes the app and routes, uses the `getOpenAPI31Document` method to generate an OpenAPI Specification version 3.1 schema object, converts the schema object to a YAML string, and saves it as a file. Add the following script to your `package.json` file: ```json filename="package.json" "create:openapi": "npx tsx ./src/generateOpenAPIYamlFile.ts" ``` Run the script using the following command: ```bash filename="Terminal" npm run create:openapi ``` An `openapi.yaml` file will be created in your root folder. ### Linting the OpenAPI document with Speakeasy The Speakeasy CLI has an OpenAPI [linting](/docs/prep-openapi/linting#configuration) command that checks the OpenAPI document for errors and style issues. Run the linting command: ```bash filename="Terminal" speakeasy lint openapi --schema ./openapi.yaml ``` A lint report will be displayed in the terminal, showing errors, warnings, and hints: ![Speakeasy lint report](/assets/openapi/hono/speakeasy-lint-report.png) The Speakeasy Linter has a set of [rules](/docs/prep-openapi/linting#available-rules) that you can configure. ### Improving and customizing the OpenAPI document using Speakeasy overlays and transformations Speakeasy [transformations](/docs/prep-openapi/transformations) are predefined functions that modify the OpenAPI document to improve it for SDK generation. You can use them to remove unused components, filter operations, and format the OpenAPI document. [Overlays](/docs/prep-openapi/overlays/create-overlays) let you create overlay documents that can be merged with an OpenAPI document, allowing you to update and use an OpenAPI document without modifying the original OpenAPI document. Overlays are useful when the same OpenAPI document is used in multiple places. Common use cases include adding Speakeasy extensions, adding examples, and hiding internal APIs from a public SDK. ### Creating an SDK from the Speakeasy CLI We'll use the [`quickstart`](/docs/speakeasy-cli/quickstart) command for a guided SDK setup. Run the command using the Speakeasy CLI: ```bash filename="Terminal" speakeasy quickstart ``` Following the prompts, provide the OpenAPI document location, name the SDK `SDK`, and select `TypeScript` as the SDK language. In the terminal, you'll see the steps taken by Speakeasy to create the SDK. ``` │ Workflow - success │ └─Target: sdk - success │ └─Source: Users API - success │ └─Validating Document - success │ └─Diagnosing OpenAPI - success │ └─Tracking OpenAPI Changes - success │ └─Snapshotting OpenAPI Revision - success │ └─Storing OpenAPI Revision - success │ └─Validating gen.yaml - success │ └─Generating Typescript SDK - success │ └─Setup Environment - success │ └─Load and Validate Document - success │ └─Generate SDK - success │ └─Compile SDK - success │ └─Setup Environment - success │ └─Load and Validate Document - success │ └─Generate SDK - success │ └─Generating Code Samples - success │ └─Snapshotting Code Samples - success │ └─Snapshotting Code Samples - success │ └─Uploading Code Samples - success ``` Speakeasy [validates](/docs/sdks/core-concepts#validation) the OpenAPI document to check that it's ready for code generation. Validation issues will be printed in the terminal. The generated SDK will be saved as a folder in your project. If you get ESLint styling errors, run the `speakeasy quickstart` command from outside your project. Speakeasy also suggests improvements for your SDK using [Speakeasy Suggest](/docs/prep-openapi/maintenance), which is an AI-powered tool in Speakeasy Studio. You can see suggestions by opening the link to your Speakeasy Studio workspace in the terminal: ![Speakeasy Studio showing SDK improvement suggestions](/assets/openapi/hono/speakeasy-studio-suggestions.png) ## Adding SDK generation to your GitHub Actions The Speakeasy [`sdk-generation-action`](https://github.com/speakeasy-api/sdk-generation-action) repository provides workflows for integrating the Speakeasy CLI into CI/CD pipelines to automatically regenerate SDKs when the OpenAPI document changes. You can set up Speakeasy to automatically push a new branch to your SDK repositories so that your engineers can review and merge the SDK changes. For an overview of how to set up automation for your SDKs, see the Speakeasy [SDK Workflow Matrix](/docs/workflow-reference/generation-reference). ## Using your SDK Once you've generated your SDK, you can [publish](/docs/publish-sdk) it for use. For TypeScript, you can publish it as an npm package. A quick, non-production-ready way to see your SDK in action is to copy your SDK folder to a frontend TypeScript project and use it there. For example, you can create a Vite project that uses TypeScript: ```bash filename="Terminal" npm create vite@latest ``` Copy the SDK folder from your Hono app to the `src` directory of your TypeScript Vite project and delete the SDK folder in your Hono project. In the SDK `README.md` file, you'll find documentation about your Speakeasy SDK. TypeScript SDKs generated with Speakeasy include an installable [Model Context Protocol (MCP) server](https://www.speakeasy.com/docs/standalone-mcp/build-server) where the various SDK methods are exposed as tools that AI applications can invoke. Your SDK documentation includes instructions for installing the MCP server. Note that the SDK is not ready for production use. To get it production-ready, follow the steps outlined in your Speakeasy workspace. The SDK includes Zod as a bundled dependency, as can be seen in the `sdk-typescript/package.json` file. Replace the code in the `src/main.ts` file with the following example code taken from the `sdk-typescript/docs/sdks/users/README.md` file: ```typescript filename="main.ts" import { SDK } from "./sdk-typescript/src/"; // Adjust the path as necessary eg if your generated SDK has a different name const sdk = new SDK(); async function run() { const result = await sdk.users.getUsers(); // Handle the result console.log({ result }); } run(); ``` Run the Vite dev server: ```bash filename="Terminal" npm run dev ``` Enable CORS in your Hono dev server by importing the built-in CORS middleware in the `src/app.ts` file: ```typescript filename="app.ts" import { cors } from "hono/cors"; ``` Add the middleware and set the `origin` to the Vite dev server URL: ```typescript filename="app.ts" app.use( "/users", cors({ origin: "http://localhost:5173", }), ); ``` Run the Hono dev server as well: ```bash filename="Terminal" npm run dev ``` You'll see the following logged in your browser dev tools console: ``` { "result": [ { "id": "123", "name": "John Doe", "age": 42 }, { "id": "124", "name": "Sarah Jones", "age": 32 } ] } ``` The SDK functions are type safe and include TypeScript autocompletion for arguments and outputs. If you try to access a property that doesn't exist: ```typescript filename="main.ts" const userOne = result[0].surname; ``` You'll get a TypeScript error: ``` Property 'surname' does not exist on type 'UserSelect' ``` ## Further reading This guide covered the basics of generating an OpenAPI document using Hono. Here are some resources to help you learn more about OpenAPI, the Hono Zod OpenAPI middleware, and Speakeasy: - [The Hono Zod OpenAPI middleware documentation](https://github.com/honojs/middleware/tree/main/packages/zod-openapi): Learn more about generating an OpenAPI document and validating values and types using Zod. The topics covered include setup, handling validation errors, configuration, RPC mode, and authorization setup. - [The Speakeasy documentation](/docs): Speakeasy has extensive documentation covering how to generate SDKs from OpenAPI documents, how to customize SDKs, and more. - [The Speakeasy OpenAPI reference](/openapi): View a detailed reference for the OpenAPI Specification. - [The Speakeasy Blog](/blog): Read articles about OpenAPI, SDK generation, and more, including: - [Native JSONL support in your SDKs](/blog/release-jsonl-support) - [Introducing comprehensive SDK testing](/blog/release-sdk-testing) - [Model Context Protocol: TypeScript SDKs for the agentic AI ecosystem](/blog/release-model-context-protocol) # How To Generate a OpenAPI for Laravel Source: https://speakeasy.com/openapi/frameworks/laravel import { Callout, YouTube } from "@/mdx/components"; You're investing in your API, and that means finally creating an OpenAPI document that accurately describes your API. With the rise in popularity of API-first design some APIs might have declared their OpenAPI before writing the code, but for many the code-first workflow is still fundamental for older APIs. If you're working with an existing Laravel application, you can generate a complete OpenAPI document directly from the API's source code. A few excellent tools have come and gone over the years, but these days [Scribe](https://scribe.knuckles.wtf/laravel) is the go to for generating API documentation form Laravel source code, and it happily exports OpenAPI to be used in a variety of other tools: like Speakeasy. ## What is Scribe all about Scribe is a robust documentation solution for PHP APIs. It helps you generate comprehensive human-readable documentation from your Laravel/Lumen/Dingo codebase, without needing to add docblocks or annotations for **everything** like other tools have required in the past. Scribe introspects the API source code itself, and without AI fudging the results it will accurately turn routing, controllers, Eloquent models, and all sorts of code into the best and most accurate API descriptions possible. Then it can be exported as OpenAPI, or Postman collections (if you're into that sort of thing.) The first step is to install a package, and explore the options available. ```bash composer require --dev knuckleswtf/scribe ``` Once installed, publish the package configuration to access the full variety of config options. ```bash php artisan vendor:publish --tag=scribe-config ``` There are a lot of [config options](https://scribe.knuckles.wtf/laravel/reference/config) available, and we'll look at some good ones later. For now let's see what a basic generation looks like. ```bash php artisan scribe:generate ``` The command above will generate both HTML documentation and an OpenAPI specification file. By default, the OpenAPI document will be saved in `storage/app/private/scribe/openapi.yaml`, but the command will let you know exactly where it's stored. ```yaml openapi: 3.0.3 info: title: 'Laravel API Documentation' description: '' version: 1.0.0 servers: - url: 'http://localhost' tags: - name: Endpoints description: '' paths: /api/health: get: summary: '' operationId: getApiHealth description: '' responses: 200: description: '' content: application/json: schema: type: object properties: status: type: string version: type: string timestamp: type: string tags: - Endpoints security: [] /api/drivers: get: summary: 'Display a listing of the resource.' operationId: displayAListingOfTheResource description: '' responses: 200: description: '' content: application/json: schema: type: object properties: data: type: array items: type: object properties: id: type: integer name: type: string code: type: string created_at: type: string updated_at: type: string meta: type: object properties: count: type: integer tags: - Endpoints security: [] '/api/drivers/{id}': get: summary: 'Display the specified resource.' operationId: displayTheSpecifiedResource description: '' responses: 200: description: '' content: application/json: schema: type: object properties: data: type: object properties: id: type: integer name: type: string code: type: string created_at: type: string updated_at: type: string tags: - Endpoints security: [] parameters: - in: path name: id description: 'The ID of the driver.' required: true schema: type: integer /api/circuits: get: summary: 'Display a listing of the resource.' operationId: displayAListingOfTheResource description: '' responses: 200: description: '' content: application/json: schema: type: object properties: data: type: array items: type: object properties: id: type: integer name: type: string location: type: string created_at: type: string updated_at: type: string meta: type: object properties: count: type: integer tags: - Endpoints security: [] post: summary: 'Store a newly created resource in storage.' operationId: storeANewlyCreatedResourceInStorage description: '' responses: { } tags: - Endpoints requestBody: required: true content: application/json: schema: type: object properties: name: type: string description: '' nullable: false location: type: string description: '' nullable: false required: - name - location security: [] '/api/circuits/{id}': get: summary: 'Display the specified resource.' operationId: displayTheSpecifiedResource description: '' responses: 200: description: '' content: application/json: schema: type: object properties: data: type: object properties: id: type: integer name: type: string location: type: string created_at: type: string updated_at: type: string tags: - Endpoints security: [] parameters: - in: path name: id description: 'The ID of the circuit.' required: true schema: type: integer /api/races: get: summary: 'Display a listing of the resource.' operationId: displayAListingOfTheResource description: '' responses: 200: description: '' content: application/json: schema: type: object properties: data: type: array items: type: object properties: id: type: integer name: type: string race_date: type: string season: type: string created_at: type: string updated_at: type: string links: type: object properties: self: type: string circuit: type: string drivers: type: string meta: type: object properties: count: type: integer tags: - Endpoints security: [] '/api/races/{id}': get: summary: 'Display the specified resource.' operationId: displayTheSpecifiedResource description: '' responses: 200: description: '' content: application/json: schema: type: object properties: data: type: object properties: id: type: integer name: type: string race_date: type: string season: type: string created_at: type: string updated_at: type: string links: type: object properties: self: type: string circuit: type: string drivers: type: string tags: - Endpoints security: [] parameters: - in: path name: id description: 'The ID of the race.' example: 1 required: true schema: type: integer ``` A surprisingly good start for something that's had absolutely no work done on it. Beyond just outputting endpoints and models, Scribe was able to look through the [API resources](https://laravel.com/docs/12.x/eloquent-resources) (also known as serializers) to figure out what the response payloads would look like, and describe them as [OpenAPI Schema objects](https://learn.openapis.org/specification/content.html#the-schema-object). Examples were also generated based on the data in the database. This is a great touch at providing some realism immediately, but it made the above example too big to share. The examples generated are based on Laravel's [database seeders](https://laravel.com/docs/12.x/seeding), so they should be more realistic than most hand-written examples - unless the seeds are creating bad or outdated data. Here's how the examples were generated. ```yaml '/api/drivers/{id}': get: summary: 'Display the specified resource.' operationId: displayTheSpecifiedResource description: '' parameters: [] responses: 200: description: '' content: application/json: schema: type: object example: data: id: 1 name: 'Max Verstappen' code: VER created_at: '2025-10-29T17:21:39.000000Z' updated_at: '2025-10-29T17:21:39.000000Z' properties: data: type: object properties: id: type: integer example: 1 name: type: string example: 'Max Verstappen' code: type: string example: VER created_at: type: string example: '2025-10-29T17:21:39.000000Z' updated_at: type: string example: '2025-10-29T17:21:39.000000Z' ``` However, there are some shortcomings to this auto-generated output. OpenAPI is used for a lot of different purposes, but in order to use it for API documentation it needs to have useful descriptions that provide context to the raw data. Currently the API missing a lot of the "why" and "how" in this sea of "what", and needs more human input. Additionally, the descriptions and summaries are all the same generic content pulled from some template, and having `summary: 'Display a listing of the resource.'` for each operation (which in turn is giving poor `operationId`) is not only going to be confusing for users, it will produce a bad SDK in Speakeasy. Let's look at some ways we can improve this output with quick config settings, and then by adding some attributes to the controllers to improve things further. ## Configuring Scribe Open the `config/scribe.php` file that was published earlier, and look for the following options: ```php // The HTML for the generated documentation. 'title' => 'F1 Race API', // A short description of your API. Will be included in the docs webpage, Postman collection and OpenAPI spec. 'description' => '', // Text to place in the "Introduction" section, right after the `description`. Markdown and HTML are supported. 'intro_text' => <<<INTRO This documentation aims to provide all the information you need to work with our API. <aside>As you scroll, you'll see code examples for working with the API in different programming languages in the dark area to the right (or as part of the content on mobile). You can switch the language used with the tabs at the top right (or from the nav menu at the top left on mobile).</aside> INTRO, ``` Updating these options will help give some context to the API consumers, but to get the rest of the API covered Scribe will need some extra context spread around the codebase. ## Creating summaries and descriptions Scribe scans application routes to identify which endpoints should be described, then extracts metadata from the corresponding routes, such as route names, URI patterns, HTTP methods. It can do this by looking purely at the code, but extra information can be added using annotations and comments in the controller to expand on the "why" and "how" of the API. In order to provide that context, Scribe looks at "docblock" comments (`/**`) on the controller methods. Take a look at the `HealthController` because that has no descriptions or summary so far. ```php focus=10:16 # app/Http/Controllers/HealthController.php namespace App\Http\Controllers\Api; use App\Http\Controllers\Controller; use Illuminate\Http\JsonResponse; class HealthController extends Controller { /** * Healthcheck * * Check that the service is up. If everything is okay, you'll get a 200 OK response. * * Otherwise, the request will fail with a 400 error, and a response listing the failed services. */ public function show(): JsonResponse { return response()->json([ 'status' => 'healthy', 'version' => 'unversioned', 'timestamp' => now()->toIso8601String(), ]); } } ``` Now when `php artisan scribe:generated` is run again, the `/api/health` endpoint will have a proper summary and description. The summary is taken from the first line of the docblock, and the description is taken from the rest of the docblock, which will work just as well in traditional PHP documentation tools as well a the OpenAPI documentation tools after export. ```yaml /api/health: get: summary: Healthcheck operationId: healthcheck description: "Check that the service is up. If everything is okay, you'll get a 200 OK response.\n\nOtherwise, the request will fail with a 400 error, and a response listing the failed services." parameters: [] # ... ``` Looking at another example, the races collection has some default state and optional values, and the description can be improved to reflect that. ```php focus=10:14 namespace App\Http\Controllers\Api; use App\Http\Controllers\Controller; use App\Http\Resources\RaceCollection; use App\Models\Race; use Illuminate\Http\Request; class RaceController extends Controller { /** * Get races * * A collection of race resources, newest first, optionally filtered by circuit or season query parameters. */ public function index(Request $request): RaceCollection { $query = Race::query(); if ($request->has('circuit')) { $query->where('circuit_id', $request->circuit); } if ($request->has('season')) { $query->where('season', $request->season); } return new RaceCollection($query->get()); } ``` Now the `description`, `summary`, and `operationId`, are all much better, because instead of just saying "it returns stuff" it's providing a hint about the default state and sorting of the data being returned, and it's immediately pointing out some options that are likely to be of interest. ```yaml /api/races: get: summary: 'Get races' operationId: getRaces description: 'A collection of race resources, newest first, optionally filtered by circuit or season query parameters.' ``` With descriptions covered, let's properly document the stuff these descriptions have been eluding to so far. Instead of jamming it into the description, we can use attributes to take advantage of more Scribe and OpenAPI functionality. ### Adding tags In OpenAPI, `tags` are used to group related operations together. Typically, a good way to use tags is to have one tag per "resource" and then associate all the relevant operations that access and modify that resource together. ```php use Knuckles\Scribe\Attributes\{Authenticated, Group, BodyParam, QueryParam}; #[Group(name: 'Races', description: 'A series of endpoints that allow programmatic access to managing F1 races.')] class RaceController extends Controller { // ... ``` Now instead of seeing `tags: [Endpoints]` in this controllers endpoints, the tag will be `Races`, and the description will be included in the OpenAPI document as a tag description. **Learn more about [OpenAPI tags](/openapi/tags).** ## Documenting parameters The whole point of an API is being able to send and receive data, so describing and documenting [API parameters](/api-design/parameters) for an endpoint is crucial. OpenAPI supports [several types of parameters](/openapi/requests/parameters), with the most common being `path`, `query`, and `header` parameters. Scribe can [automatically describe path parameters](https://scribe.knuckles.wtf/laravel/documenting/url-parameters) by looking at the route definitions, but query parameters and others need to be documented manually. It's best practice to document all types of parameters manually because the automatic generation is only spotting if its optional or not, so it will still need a description. In the race API, the `RaceController@index` method supports two optional query parameters: `season` and `circuit`. Let's document those using Scribe's `QueryParam` attribute. ```php focus=6:7 /** * Get races * * A collection of race resources, newest first, optionally filtered by circuit or season query parameters. */ #[QueryParam(name: 'season', type: 'string', description: 'Filter the results by season year', required: false, example: '2024')] #[QueryParam(name: 'circuit', type: 'string', description: 'Filter the results by circuit name', required: false, example: 'Monaco')] public function index(Request $request): RaceCollection { ``` The result of the above will be the following inside your OpenAPI specification: ```yaml summary: 'Get races' operationId: getRaces description: 'A collection of race resources, newest first, optionally filtered by circuit or season query parameters.' parameters: - in: query name: season description: 'Filter the results by season year' example: '2024' required: false schema: type: string description: 'Filter the results by season year' example: '2024' nullable: false - in: query name: circuit description: 'Filter the results by circuit name' example: Monaco required: false schema: type: string description: 'Filter the results by circuit name' example: Monaco nullable: false ``` API consumers looking at the docs will be able to see what query parameters are available, what they do, and some examples of how to use them, which can really speed up adoption. Scribe will set `in: query` for any `QueryParam` parameters, and `in: path` for `UrlParam` parameters. <Callout type="info" title="Note on duplication"> The example and description are repeated in both the parameter definition and the schema definition, which is not required by OpenAPI itself, but its the most compatible way to ensure all tools can read the information correctly, and seeing as its automatically generated it doesn't hurt to have the duplication. </Callout> ## Documenting request bodies APIs also need to accept data, and in RESTful APIs this is typically done through `POST`, `PUT`, and `PATCH` requests that contain a request body. Scribe can automatically generate request body schemas by looking at Laravel's [form request validation rules](https://laravel.com/docs/12.x/validation#form-request-validation), but similar to the earlier examples the generated output is very bare-bones and needs some extra context. Given the `RaceController` has a bog standard `store` method for creating new races, let's see how the automatic generation looks first. ```php /** * Create a race * * Allows authenticated users to submit a new Race resource to the system. */ public function store(Request $request): RaceResource { $validated = $request->validate([ 'name' => 'required|string', 'circuit_id' => 'required|integer|exists:circuits,id', 'race_date' => 'required|date', 'season' => 'nullable|string', 'driver_ids' => 'sometimes|array', 'driver_ids.*' => 'integer|exists:drivers,id', ]); $race = Race::create([ 'name' => $validated['name'], 'circuit_id' => $validated['circuit_id'], 'race_date' => $validated['race_date'], 'season' => $validated['season'] ?? null, ]); if (isset($validated['driver_ids'])) { $race->drivers()->attach($validated['driver_ids']); } return new RaceResource($race); } ``` This will generate the following OpenAPI for the `POST /api/races` endpoint: ```yaml post: summary: 'Create a race' operationId: createARace description: 'Allows authenticated users to submit a new Race resource to the system.' parameters: [] responses: { } tags: - Endpoints requestBody: required: true content: application/json: schema: type: object properties: name: type: string description: '' example: architecto nullable: false circuit_id: type: integer description: 'The <code>id</code> of an existing record in the circuits table.' example: 16 nullable: false race_date: type: string description: 'Must be a valid date.' example: '2025-11-16T14:53:59' nullable: false season: type: string description: '' example: architecto nullable: true driver_ids: type: array description: 'The <code>id</code> of an existing record in the drivers table.' example: - 16 items: type: integer required: - name - circuit_id - race_date ``` Not bad! Some information is being pulled from the validation rules, such as required fields and types, but the descriptions are pretty much useless, and some of the examples don't make sense. For the most part, this has been documented quite well by leaning on the Laravel framework and understanding what the validation rules on the request means. Let's enhance this by adding some information. ```php /** * Create a race * * Allows authenticated users to submit a new Race resource to the system. */ #[Authenticated] #[BodyParam(name: 'name', type: 'string', description: 'The name of the race.', required: true, example: 'Monaco Grand Prix')] #[BodyParam(name: 'race_date', type: 'string', description: 'The date and time the race takes place, RFC 3339 in local timezone.', required: true, example: '2024-05-26T14:53:59')] #[BodyParam(name: 'circuit_id', type: 'string', description: 'The Unique Identifier for the circuit where the race will be held.', required: true, example: '1234-1234-1234-1234')] #[BodyParam(name: 'season', type: 'string', description: 'The season year for this race.', required: true, example: '2024')] #[BodyParam(name: 'driver_ids', type: 'array', description: 'An array of Unique Identifiers for drivers participating in the race.', required: false, example: [ "5678-5678-5678-5678", "6789-6789-6789-6789" ])] public function store(Request $request): RaceResource ``` Let's take a look at the resulting OpenAPI for the request body now: ```yaml requestBody: required: true content: application/json: schema: type: object properties: name: type: string description: 'The name of the race.' example: 'Monaco Grand Prix' nullable: false circuit_id: type: string description: 'The Unique Identifier for the circuit where the race will be held.' example: 1234-1234-1234-1234 nullable: false race_date: type: string description: 'The date and time the race takes place, RFC 3339 in local timezone.' example: '2024-05-26T14:53:59' nullable: false season: type: string description: 'The season year for this race.' example: '2024' nullable: false driver_ids: type: array description: 'An array of Unique Identifiers for drivers participating in the race.' example: - 5678-5678-5678-5678 - 6789-6789-6789-6789 items: type: string required: - name - circuit_id - race_date - season ``` As you can see, a lot more information is provided which will help anyone who wants to interact with this API. ## Upgrading to OpenAPI v3.1 Update Scribe to v5.6.0 or later and set the OpenAPI version in `config/scribe.php`: ```php 'openapi' => [ 'version' => '3.1.3', ``` This will generate an OpenAPI v3.1 document, which has several advantages over v3.0, including support for JSON Schema, better handling of nullable types, and improved support for webhooks. ## Summary When API design-first is not an option, "catching up" with Scribe means you can quickly get to the point of having a complete OpenAPI document without having to duplicate a lot of information. Let the code do the talking, and enable tools like Speakeasy to generate SDKs, tests, and more. # How to generate an OpenAPI document with NestJS Source: https://speakeasy.com/openapi/frameworks/nestjs import { Callout } from "@/mdx/components"; This guide walks you through generating an OpenAPI document for a [NestJS](https://nestjs.com/) API and using Speakeasy to create an SDK based on the generated document. <Callout title="Example code" type="info"> Clone the [Speakeasy NestJS example repo](https://github.com/speakeasy-api/nestjs-openapi-example.git) to follow along with the example code in this tutorial. The `initial-app` branch has the initial state of the app that we'll use to start this tutorial. </Callout> Here's what we'll do: 1. Add the NestJS OpenAPI module to a NestJS project. 2. Generate an OpenAPI document using the NestJS OpenAPI module. 3. Improve the OpenAPI document for better downstream SDK generation. 4. Use the Speakeasy CLI to generate an SDK based on the OpenAPI document. 5. Use a Speakeasy OpenAPI extension to improve the generated SDK. We'll also take a look at how you can use the generated SDK. Your NestJS project might not be as simple as our example app, but the steps below should translate well to any NestJS project. ## The SDK generation pipeline NestJS has an [OpenAPI (Swagger) module](https://github.com/nestjs/swagger) for generating OpenAPI documents. We'll begin by installing, configuring, and initializing the NestJS [OpenAPI (Swagger) module](https://github.com/nestjs/swagger). We will also use the [Scalar UI](https://www.npmjs.com/package/@scalar/nestjs-api-reference) to add an interactive documentation UI for the API. The quality of your OpenAPI document determines the quality of generated SDKs and documentation, so we'll look into ways you can improve the generated document based on the Speakeasy [OpenAPI best practices](/docs/best-practices). We'll then use the improved OpenAPI document to generate an SDK using Speakeasy. Finally, we'll use a simplified example to demonstrate how to use the SDK we generated and how to add SDK generation to a CI/CD pipeline so that Speakeasy automatically generates fresh SDKs whenever your NestJS API changes in the future. ## Requirements This guide assumes that you have an existing NestJS app (or a clone of our [example application](https://github.com/speakeasy-api/nestjs-openapi-example.git)) and basic familiarity with NestJS. The following should be installed on your machine: - [Node.js version 16 or above](https://nodejs.org/en/download) (we used Node v20.17.0). - The [NestJS CLI](https://docs.nestjs.com/cli/overview), which can be installed with the following command once you have Node.js: ```bash filename="Terminal" npm install -g @nestjs/cli ``` - The [JS-YAML](https://www.npmjs.com/package/js-yaml) package, which we'll use to convert the OpenAPI document to a YAML file. - The [Speakeasy CLI](/docs/speakeasy-cli/getting-started), which we'll use to generate an SDK from the OpenAPI document. ## Adding `@nestjs/swagger` to a NestJS project Install the NestJS OpenAPI (Swagger) and Scalar API Reference modules: ```bash filename="Terminal" npm install --save @nestjs/swagger @scalar/nestjs-api-reference ``` In the `bootstrap` function of your application entry file, initialize Swagger using the `SwaggerModule` class: ```typescript filename="main.ts" const config = new DocumentBuilder() .setTitle('Pet API') .setDescription('Create a cat or dog record and view pets by id') .setVersion('1.0') .addTag('library') .build(); const document = SwaggerModule.createDocument(app, config); // serializable object - conform to OpenAPI SwaggerModule.setup('api', app, document, { swaggerUiEnabled: false, }); app.use( '/api', apiReference({ spec: { content: document, }, }), ); ``` Add the required imports: ```typescript filename="main.ts" import { SwaggerModule, DocumentBuilder } from '@nestjs/swagger'; import { apiReference } from '@scalar/nestjs-api-reference'; ``` In the above code: - The `SwaggerModule.createDocument` method returns a serializable [OpenAPI document](https://swagger.io/specification/#openapi-document) object that we'll convert to an OpenAPI YAML document file using JS-YAML. - We use `DocumentBuilder` to create the base structure of the OpenAPI document. The [`DocumentBuilder` methods](https://github.com/nestjs/swagger/blob/master/lib/document-builder.ts) set the properties that identify the purpose and owner of the document, such as the title and description properties. - We use the `createDocument()` method to define the API routes by passing in two arguments: the `app` instance and the document `config`. We can also provide a third argument, [`SwaggerDocumentOptions`](https://docs.nestjs.com/openapi/introduction#document-options). - We use `SwaggerModule.setup()` to expose the OpenAPI document at `/api-yaml` for the YAML format and `/api-json` for the JSON format. - The `app.use('/api', ...)` method mounts the Scalar API Reference component to the `/api` route. The `apiReference` function takes the `document` object as a parameter, which represents the OpenAPI document. Run the NestJS HTTP development server: ```bash filename="Terminal" npm run start:dev ``` Open your browser and navigate to [`http://localhost:3000/api`](http://localhost:3000/api). You should see the Scalar UI with three API endpoints in the sidebar. ![Scalar UI](/assets/openapi/nestjs/scalar-ui.png) For each API endpoint, you can see which parameters are required and try out the different API endpoints. Open `http://localhost:3000/api-yaml` to see the following basic OpenAPI document in YAML format: ```yaml openapi: 3.0.0 paths: /pets: post: operationId: PetsController_create parameters: [] responses: "201": description: "" /pets/cats/{id}: get: operationId: PetsController_findOneCat parameters: - name: id required: true in: path schema: type: string responses: "200": description: "" /pets/dogs/{id}: get: operationId: PetsController_findOneDog parameters: - name: id required: true in: path schema: type: string responses: "200": description: "" info: title: Pet API description: Create a cat or dog record and view pets by id version: "1.0" contact: {} tags: - name: library description: "" servers: [] components: schemas: {} ``` Note that the document uses OpenAPI Specification version 3.0.0. ## Supported OpenAPI Specification versions in NestJS and Speakeasy Speakeasy currently supports the OpenAPI Specification versions 3.0.x and 3.1.x. We recommend using OpenAPI Specification version 3.1 if possible, as it's fully compatible with [JSON Schema](https://json-schema.org/), which gives you access to a large ecosystem of tools and libraries. NestJS supports OpenAPI Specification version 3.0.x. ## Adding OpenAPI `info` in NestJS Let's improve the OpenAPI document by better describing it. We'll add more fields to the [info object](https://swagger.io/specification/#info-object), which contains metadata about the API. Add the following `DocumentBuilder` methods to the `config` section of the document (`main.ts`) to supply more data about the API: ```typescript filename="main.ts" .setContact( 'Speakeasy support', 'http://www.example.com/support', 'support@example.com', ) .setTermsOfService('http://example.com/terms/') .setLicense( 'Apache 2.0', 'https://www.apache.org/licenses/LICENSE-2.0.html', ) ``` ## Updating NestJS to generate OpenAPI components schemas In the example app, [NestJS core decorators](https://docs.nestjs.com/custom-decorators) define the structure and functionality of the Pets Controller and its API routes. We can add [OpenAPI decorators](https://docs.nestjs.com/openapi/decorators) to better describe our API. The OpenAPI document lacks details about the POST request body, data schema, and API response. Add the following OpenAPI decorators, with the `Api` prefix to distinguish them from the core decorators, to the `@Get('cats/:id')` route handler: ```typescript filename="pets.controller.ts" @ApiOperation({ summary: 'Get cat' }) @ApiResponse({ description: 'The found record', type: Cat, }) @ApiBadRequestResponse({ description: 'Bad Request' }) ``` Import these decorators from `'@nestjs/swagger'`: ```typescript filename="pets.controller.ts" import { ApiBadRequestResponse, ApiBody, ApiExtension, ApiForbiddenResponse, ApiOkResponse, ApiOperation, ApiResponse, ApiTags, getSchemaPath, } from '@nestjs/swagger'; ``` Add the following OpenAPI decorators to the `@Get('dogs/:id')` route handler: ```typescript filename="pets.controller.ts" @ApiOperation({ summary: 'Get dog' }) @ApiResponse({ description: 'The found record', type: Dog, }) @ApiBadRequestResponse({ description: 'Bad Request' }) ``` Add the following OpenAPI decorators to the `@Post()` route handler: ```typescript filename="pets.controller.ts" @ApiOperation({ summary: 'Create a pet' }) @ApiBody({ schema: { oneOf: [{ $ref: getSchemaPath(Cat) }, { $ref: getSchemaPath(Dog) }], discriminator: { propertyName: 'type', mapping: { cat: getSchemaPath(Cat), dog: getSchemaPath(Dog), }, }, }, description: 'Create a pet cat or dog', }) @ApiOkResponse({ schema: { oneOf: [{ $ref: getSchemaPath(Cat) }, { $ref: getSchemaPath(Dog) }], discriminator: { propertyName: 'type', mapping: { cat: getSchemaPath(Cat), dog: getSchemaPath(Dog), }, }, }, }) @ApiForbiddenResponse({ description: 'Forbidden' }) @ApiBadRequestResponse({ description: 'Bad Request' }) ``` The `@ApiBody()` and `@ApiOkResponse` decorators use the [Schema Object](https://swagger.io/specification/#schemaObject), which defines the input and output data types. The allowed data types are defined by the `Cat` and `Dog` data transfer objects (DTO) schema. A DTO schema defines how data will be sent over the network. Now, run the NestJS HTTP server and open `http://localhost:3000/api-yaml/`. You'll see the OpenAPI endpoints description is more fleshed out. The POST request originally looked like the following: ```yaml /pets: post: operationId: PetsController_create parameters: [] responses: "201": description: "" ``` It should now look as follows: ```yaml focus=6:38 /pets: post: operationId: PetsController_create summary: Create pet parameters: [] requestBody: required: true description: Create a pet cat or dog content: application/json: schema: oneOf: - $ref: "#/components/schemas/Cat" - $ref: "#/components/schemas/Dog" discriminator: propertyName: type mapping: cat: "#/components/schemas/Cat" dog: "#/components/schemas/Dog" responses: "200": description: "" content: application/json: schema: oneOf: - $ref: "#/components/schemas/Cat" - $ref: "#/components/schemas/Dog" discriminator: propertyName: type mapping: cat: "#/components/schemas/Cat" dog: "#/components/schemas/Dog" "400": description: Bad Request "403": description: Forbidden ``` The [Reference Object](https://swagger.io/specification/#reference-object) (`$ref`) is a reference identifier that specifies the location, as a URI, of the value being referenced. It references the `schemas` field of the [Components Object](https://swagger.io/specification/#components-object), which holds reusable schema objects. If you look at the `components` schema, you'll see the `properties` objects are empty. ```yaml mark=5,8 components: schemas: Cat: type: object properties: {} Dog: type: object properties: {} ``` To make the model properties visible to the `SwaggerModule`, we can annotate each property using the `@ApiProperty()` decorator. For example: ```typescript filename="cat.entity.ts" mark=1 @ApiProperty({ example: 'Panama', description: 'The name of the cat' }) @IsString() readonly name: string; ``` This can be tedious, especially with medium- to large-sized projects. You can use the NestJS [Swagger CLI plugin](https://docs.nestjs.com/openapi/cli-plugin#cli-plugin) to automate this annotation. To enable the plugin, open your `nest-cli.json` file, add the following `plugins` configuration, and restart the server: ```json filename="nest-cli.json" mark=7 { "$schema": "https://json.schemastore.org/nest-cli", "collection": "@nestjs/schematics", "sourceRoot": "src", "compilerOptions": { "deleteOutDir": true, "plugins": ["@nestjs/swagger"] } } ``` You'll now see that the `properties` fields are populated as follows: ```yaml components: schemas: Cat: type: object properties: type: type: string name: type: string age: type: number breed: type: string environment: type: string enum: - indoor - outdoor required: - type - name - age - breed - environment Dog: type: object properties: type: type: string name: type: string age: type: number breed: type: string size: type: string enum: - small - medium - large required: - type - name - age - breed - size ``` The plugin annotates all DTO properties with the `@ApiProperty` decorator, sets the `type` or `enum` property depending on the type, sets validation rules based on `class-validator` decorators, and carries out various other [automated actions](https://docs.nestjs.com/openapi/cli-plugin#overview). You can generate descriptions for properties and endpoints, and create example values for properties based on comments: ```typescript filename="cat.entity.ts" mark=1:4 /** * The type of pet * @example 'cat' */ @IsEnum(['cat']) readonly type: 'cat'; ``` For this to work, `introspectComments` must be set to `true` in the `options` property of the plugin: ```json filename="nest-cli.json" "plugins": [ { "name": "@nestjs/swagger", "options": { "introspectComments": true } } ] ``` The example and description will then be added to the OpenAPI document `components` schema: ```yaml focus=8:9 components: schemas: Cat: type: object properties: type: type: string description: The type of pet example: cat ``` Add comments that provide a description and example value for each property of the `Cat` and `Dog` entities. The Scalar UI will allow you to access the example value for the request body and a successful response. Click on the **Test Request** button of the request to display a modal, where you can try a request to the endpoint: ![Scalar UI Try Request](/assets/openapi/nestjs/scalar-ui-try-request.png) You can see an example of a request body in the **Body** section of the modal: ![Scalar UI example request body](/assets/openapi/nestjs/scalar-ui-example-req-body.png) By toggling the **200** option under **Responses**, you can also see the details of the example data schemas: ![Scalar UI example schema](/assets/openapi/nestjs/scalar-ui-example-schema.png) ## Customizing the OpenAPI `operationId` with NestJS In the OpenAPI document, each HTTP request has an `operationId` that identifies the operation. In SDKs, the `operationId` is also used to generate method names and documentation. By default, the NestJS OpenAPI (Swagger) module uses the NestJS `controllerKey` and `methodKey` to name the `operationID` something like `PetsController_findOneDog`. A long operation name is not ideal. We can use the `operationIdFactory` method in the [Swagger document options](https://docs.nestjs.com/openapi/introduction#document-options) to instruct the module to generate more concise names using only the `methodKey`. Define the following `options` in the `bootstrap` function: ```typescript filename="main.ts" const options: SwaggerDocumentOptions = { operationIdFactory: (controllerKey: string, methodKey: string) => methodKey, }; ``` Pass the `options` to the `SwaggerModule` as follows: ```typescript filename="main.ts" const document = SwaggerModule.createDocument(app, config, options); ``` Import `SwaggerDocumentOptions` from `@nestjs/swagger`: ```typescript filename="main.ts" focus=4 import { SwaggerModule, DocumentBuilder, SwaggerDocumentOptions, } from '@nestjs/swagger'; ``` ## Adding OpenAPI tags to NestJS routes Whether you're building a big application or only have a handful of operations, we recommend adding tags to all your NestJS routes so you can group them by tag in the generated SDK code and documentation. ### Adding tags To add an OpenAPI tag to a route in NestJS, add the `@ApiTags` decorator: ```typescript filename="pets.controller.ts" @Get('cats/:id') @ApiTags('cats') ``` ### Adding metadata to tags We've already added metadata to the `@ApiTags('cats')` decorator using other decorators provided by `@nestjs/swagger`, such as `@ApiOperation` and `@ApiResponse`. We can add metadata to the root tag field in the OpenAPI document. Add the following to the `config` section of the OpenAPI document: ```typescript filename="main.ts" .addTag('Pets', 'Pets operations', { url: 'https://example.com/api', description: 'Operations API endpoint', }) ``` You can add more than one tag by using additional `.addTag()` method calls: ```typescript filename="main.ts" .addTag('cats') .addTag('dogs') ``` ## Adding a list of servers to the NestJS OpenAPI document When validating an OpenAPI document, Speakeasy expects a list of servers at the root of the OpenAPI document. We'll add a server using the `DocumentBuilder` method, `addServer()`. Insert the `addServer()` method in the `config` of the OpenAPI document: ```typescript filename="main.ts" .addServer('http://localhost:3000/', 'Development server') ``` ## Adding retries to your SDK with `x-speakeasy-retries` [OpenAPI document extensions](/openapi/extensions) allow us to add vendor-specific functionality to the OpenAPI document. - Extension fields must be prefixed with `x-`. - Speakeasy uses extensions that start with `x-speakeasy-`. Let's use a Speakeasy extension that adds retries to requests from Speakeasy SDKs by adding a top-level `x-speakeasy-retries` schema to our OpenAPI document. We can also override the retry strategy per operation. ### Adding global retries Apply the Speakeasy retries extension globally by adding the `addExtension()` method from `DocumentBuilder` to the `config` section of the OpenAPI document: ```typescript filename="main.ts" .addExtension('x-speakeasy-retries', { strategy: 'backoff', backoff: { initialInterval: 500, maxInterval: 60000, maxElapsedTime: 3600000, exponent: 1.5, }, statusCodes: ['5XX'], retryConnectionErrors: true, }) ``` ### Adding retries per method To create a unique retry strategy for a single route, use the `ApiExtension` decorator to add `x-speakeasy-retries` to a NestJS controller route handler as follows: ```typescript filename="pets.controller.ts" @ApiExtension('x-speakeasy-retries', { strategy: 'backoff', backoff: { initialInterval: 1000, maxInterval: 80000, maxElapsedTime: 3600000, exponent: 1.5, }, statusCodes: ['5XX'], retryConnectionErrors: true, }) ``` ## Generating an SDK based on your OpenAPI document Before creating an SDK, we need to save the NestJS-generated OpenAPI document to a file. We'll use JS-YAML to do this. ### Saving the OpenAPI document to a YAML file Add the following imports to your application entry file: ```typescript filename="main.ts" import * as yaml from 'js-yaml'; import { writeFileSync } from 'fs'; ``` In the `bootstrap` function, convert the OpenAPI document to a YAML string and save it as a file: ```typescript filename="main.ts" const yamlString = yaml.dump(document); writeFileSync('openapi.yaml', yamlString); ``` When you run the NestJS dev server, an `openapi.yaml` file will be generated in your root directory. ### Linting the OpenAPI document with Speakeasy The Speakeasy CLI has an OpenAPI [linting](/docs/linting) command that checks the OpenAPI document for errors and style issues. Run the linting command: ```bash filename="Terminal" speakeasy lint openapi --schema ./openapi.yaml ``` A lint report will be displayed in the terminal, showing errors, warnings, and hints: ![Speakeasy Lint report](/assets/openapi/nestjs/speakeasy-lint-report.png) The Speakeasy Linter uses a [recommended set of rules](/docs/linting/linting#speakeasy-recommended), which you can [configure](/docs/linting#configuration). ### Generating an SDK from the Speakeasy CLI We'll use the [`quickstart`](/docs/speakeasy-cli/quickstart) command for a guided SDK setup. Run the command using the Speakeasy CLI: ```bash filename="Terminal" speakeasy quickstart ``` Following the prompts, provide the OpenAPI document location, name the SDK `SDK`, and select `TypeScript` as the SDK language. In the terminal, you'll see the steps taken by Speakeasy to generate the SDK. ```txt │ Workflow - success │ └─Target: sdk - success │ └─Source: SDK-OAS - success │ └─Validating Document - success │ └─Diagnosing OpenAPI - success │ └─Tracking OpenAPI Changes - success │ └─Snapshotting OpenAPI Revision - success │ └─Storing OpenAPI Revision - success │ └─Computing Document Changes - success │ └─Downloading prior revision - success │ └─Computing changes - success │ └─Uploading changes report - success │ └─Validating gen.yaml - success │ └─Generating Typescript SDK - success │ └─Setup Environment - success │ └─Load and Validate Document - success │ └─Generate SDK - success │ └─Compile SDK - success │ └─Setup Environment - success │ └─Load and Validate Document - success │ └─Generate SDK - success │ └─Generating Code Samples - success │ └─Snapshotting Code Samples - success │ └─Snapshotting Code Samples - success │ └─Uploading Code Samples - success ``` Speakeasy [validates](/docs/concepts#validation) the OpenAPI document to check that it's ready for code generation. Validation issues will be printed in the terminal. The generated SDK will be saved as a folder in your project. ## Adding SDK generation to your GitHub Actions The Speakeasy [`sdk-generation-action`](https://github.com/speakeasy-api/sdk-generation-action) repository provides workflows for integrating the Speakeasy CLI into CI/CD pipelines to automatically regenerate SDKs when the OpenAPI document changes. You can set up Speakeasy to automatically push a new branch to your SDK repositories so that your engineers can review and merge the SDK changes. For an overview of how to set up automation for your SDKs, see the Speakeasy [SDK Workflow Matrix](/docs/workflow-reference/generation-reference). ## Using your SDK Once you've generated your SDK, you can [publish](/docs/publish-sdk) it for use. For TypeScript, you can publish it as an npm package. A quick, non-production-ready way to see your SDK in action is to copy your SDK folder to a frontend TypeScript project and use it there. For example, you can create a Vite project that uses TypeScript: ```bash filename="Terminal" npm create vite@latest ``` Copy the SDK folder from your NestJS app to the `src` directory of your TypeScript Vite project. Delete the SDK folder in your NestJS project. In the SDK `README.md` file, you'll find documentation about your Speakeasy SDK. Note that the SDK is not ready for production use. To get it production-ready, follow the steps outlined in your Speakeasy workspace. The SDK includes Zod as a bundled dependency, as can be seen in the `sdk-typescript/package.json` file. Replace the code in the `src/main.ts` file with the following lines of code: ```typescript filename="main.ts" import { SDK } from './sdk-typescript/src/'; // Adjust the path as necessary eg if your generated SDK has a different name import { catsFindOneCat } from './sdk-typescript/src/funcs/catsFindOneCat'; const sdk = new SDK(); async function run() { const res = await catsFindOneCat(sdk, { id: "0", }); if (!res.ok) { throw res.error; } const { value: result } = res; // Handle the result console.log(result); } run(); ``` Run the Vite dev server: ```bash filename="Terminal" npm run dev ``` Enable CORS in your NestJS dev server by adding the following configuration to the `bootstrap` function above the `await app.listen(3000);` line: ```typescript filename="main.ts" app.enableCors({ origin: 'http://localhost:5173', // Vite's default port methods: 'GET,POST,PUT,DELETE,OPTIONS', allowedHeaders: 'Content-Type, Authorization', credentials: true, }); ``` Run the NestJS dev server as well: ```bash filename="Terminal" npm run start:dev ``` You'll see the following logged in your browser dev tools console: ```javascript {type: 'cat', name: 'Shadow', age: 8, breed: 'Bombay', environment: 'indoor'} ``` The SDK functions are type safe and include TypeScript autocompletion for arguments and outputs. If you try to use an incorrect type for an argument: ```typescript filename="main.ts" const res = await catsFindOneCat(sdk, { id: 1, }); ``` You'll get a TypeScript error: ``` Type 'number' is not assignable to type 'string' ``` ## Further reading This guide covered the basics of generating an OpenAPI document using NestJS. Here are some resources to help you learn more about OpenAPI, the NestJS OpenAPI module, and Speakeasy: - [NestJS OpenAPI (Swagger) module documentation](https://typespec.io/docs): Learn more about generating an OpenAPI document using NestJS. The topics covered include types and parameters, operations, security, mapped types, and decorators. - [Speakeasy documentation](/docs): Speakeasy has extensive documentation covering how to generate SDKs from OpenAPI documents, customize SDKs, and more. - [Speakeasy OpenAPI reference](/openapi): View a detailed reference for the OpenAPI Specification. # How to generate an OpenAPI document with Pydantic V2 Source: https://speakeasy.com/openapi/frameworks/pydantic [Pydantic](https://docs.pydantic.dev/latest/) is considered by many API developers to be the best data validation library for Python, and with good reason. By defining an application's models in Pydantic, developers benefit from a vastly improved development experience, runtime data validation and serialization, and automatic OpenAPI document generation. However, many developers don't realize they can generate OpenAPI documents from their Pydantic models, which they can then use to create SDKs, documentation, and server stubs. In this guide, you'll learn how to create new Pydantic models, generate an OpenAPI document from them, and use the generated schema to create an SDK for your API. We'll start with the simplest possible Pydantic model and gradually add more features to show how Pydantic models translate to OpenAPI documents. ## Prerequisites Before we get started, make sure you have [Python](https://www.python.org/downloads/) 3.8 or higher installed on your machine. Check your Python version by running the following command: ```bash filename="Terminal" python --version ``` We use Python 3.13.3 in this guide, but any version of Python 3.8 or higher should work. ## Creating a new Python project First, create a new Python project and install the Pydantic library: ```bash filename="Terminal" # Create and open a new directory for the project mkdir pydantic-openapi cd pydantic-openapi # Create a new virtual environment python -m venv venv # Activate the virtual environment source venv/bin/activate ``` ## Install the required libraries We'll install Pydantic and PyYAML to generate and pretty-print the OpenAPI document: ```bash filename="Terminal" # Install the Pydantic library pip install pydantic # Install the PyYAML library for pretty-printing the OpenAPI schema pip install pyyaml ``` ## Pydantic to OpenAPI document walkthrough Let's follow a step-by-step process to generate an OpenAPI document from a Pydantic model without any additional libraries. ### Defining a simple Pydantic model Create a new Python file called `models.py` and define a simple Pydantic model. In this example, we define a Pydantic model called `Pet` with three fields: `id`, `name`, and `breed`. The `id` field is an integer, and the `name` and `breed` fields are strings. ```python from pydantic import BaseModel class Pet(BaseModel): id: int name: str breed: str ``` ### Generating a JSON schema for the Pydantic model Add a new function called `print_json_schema` to the `models.py` file that prints the JSON schema for the `Pet` model. This function uses the `model_json_schema` method provided by Pydantic to generate the JSON schema, which Python then prints to the console as YAML. We use YAML for readability, but the output is still a valid JSON schema. ```python import yaml from pydantic import BaseModel class Pet(BaseModel): id: int name: str breed: str def print_json_schema(): print(yaml.dump(Pet.model_json_schema())) if __name__ == "__main__": print_json_schema() ``` Run `python models.py` to generate the JSON schema for the `Pet` model and print it as YAML: ```yaml properties: breed: title: Breed type: string id: title: Id type: integer name: title: Name type: string required: - id - name - breed title: Pet type: object ``` ### Multiple Pydantic models Let's add another Pydantic model called `Owner` to the `models.py` file. The `Owner` model has two fields: `id` and `name`. Both fields are integers. Additionally, the `Owner` model has a list of `Pet` objects. ```python import yaml from pydantic import BaseModel class Pet(BaseModel): id: int name: str breed: str class Owner(BaseModel): id: int name: str pets: list[Pet] def print_json_schema(): print(yaml.dump(Pet.model_json_schema())) if __name__ == "__main__": print_json_schema() ``` ### Generating a JSON schema for multiple Pydantic models Update the `print_json_schema` function to print the JSON schema for both the `Pet` and `Owner` models. Note that we're now calling the [`models_json_schema`](https://docs.pydantic.dev/2.7/api/json_schema/#pydantic.json_schema.models_json_schema) function from `pydantic.json_schema` instead of the `model_json_schema` method. ```python import yaml from pydantic import BaseModel from pydantic.json_schema import models_json_schema class Pet(BaseModel): id: int name: str breed: str class Owner(BaseModel): id: int name: str pets: list[Pet] def print_json_schema(models): _, schemas = models_json_schema( [(model, "validation") for model in models], ) print(yaml.dump(schemas)) if __name__ == "__main__": print_json_schema([Pet, Owner]) ``` Run `python models.py` to generate the JSON schema for both the `Pet` and `Owner` models and print it as YAML: ```yaml $defs: Owner: properties: id: title: Id type: integer name: title: Name type: string pets: items: $ref: "#/$defs/Pet" title: Pets type: array required: - id - name - pets title: Owner type: object Pet: properties: breed: title: Breed type: string id: title: Id type: integer name: title: Name type: string required: - id - name - breed title: Pet type: object ``` The generated schema includes definitions for both the `Pet` and `Owner` models. The `Owner` model has a reference to the `Pet` model, indicating that the `Owner` model contains a list of `Pet` objects. Note that the root of the schema includes a `$defs` key that contains the definitions for both models, and the `Owner` model references the `Pet` model using the `$ref` keyword. ### Customizing Pydantic JSON schema generation Let's customize the generated JSON schema to reference the `Pet` model using the `#/components/schemas` path instead of `$defs`. We'll use the `ref_template` parameter of the `models_json_schema` function to specify the reference template. ```python import yaml from pydantic import BaseModel from pydantic.json_schema import models_json_schema class Pet(BaseModel): id: int name: str breed: str class Owner(BaseModel): id: int name: str pets: list[Pet] def print_json_schema(models): _, schemas = models_json_schema( [(model, "validation") for model in models], ref_template="#/components/schemas/{model}", ) print(yaml.dump(schemas)) if __name__ == "__main__": print_json_schema([Pet, Owner]) ``` Next, we'll update the `print_json_schema` function to print a JSON schema that resembles an OpenAPI document's `components` section. ```python import yaml from pydantic import BaseModel from pydantic.json_schema import models_json_schema class Pet(BaseModel): id: int name: str breed: str class Owner(BaseModel): id: int name: str pets: list[Pet] def print_json_schema(models): _, schemas = models_json_schema( [(model, "validation") for model in models], ref_template="#/components/schemas/{model}", ) openapi_schema = { "components": { "schemas": schemas.get('$defs'), } } print(yaml.dump(openapi_schema)) if __name__ == "__main__": print_json_schema([Pet, Owner]) ``` Run `python models.py` to generate the OpenAPI document for both the `Pet` and `Owner` models. The generated OpenAPI document includes the `components` section, with definitions for both the `Pet` and `Owner` models. ```yaml components: schemas: Owner: properties: id: title: Id type: integer name: title: Name type: string pets: items: $ref: "#/components/schemas/Pet" title: Pets type: array required: - id - name - pets title: Owner type: object Pet: properties: breed: title: Breed type: string id: title: Id type: integer name: title: Name type: string required: - id - name - breed title: Pet type: object ``` The JSON schema we generated resembles an OpenAPI document's `components` section, but to generate a valid OpenAPI document, we need to add the `openapi` and `info` sections. Edit the `print_json_schema` function in `models.py` to include the `openapi` and `info` sections in the generated OpenAPI document. ```python import yaml from pydantic import BaseModel from pydantic.json_schema import models_json_schema class Pet(BaseModel): id: int name: str breed: str class Owner(BaseModel): id: int name: str pets: list[Pet] def print_json_schema(models): _, schemas = models_json_schema( [(model, "validation") for model in models], ref_template="#/components/schemas/{model}", ) openapi_schema = { "openapi": "3.1.0", "info": { "title": "Pet Sitter API", "version": "0.0.1", }, "components": { "schemas": schemas.get('$defs'), } } print(yaml.dump(openapi_schema)) if __name__ == "__main__": print_json_schema([Pet, Owner]) ``` Run `python models.py` to generate the complete OpenAPI document for both the `Pet` and `Owner` models. The generated OpenAPI document includes the `openapi`, `info`, and `components` sections with definitions for both the `Pet` and `Owner` models. ```yaml openapi: 3.1.0 info: title: Pet Sitter API version: 0.0.1 components: schemas: Owner: properties: id: title: Id type: integer name: title: Name type: string pets: items: $ref: "#/components/schemas/Pet" title: Pets type: array required: - id - name - pets title: Owner type: object Pet: properties: id: title: Id type: integer name: title: Name type: string breed: title: Breed type: string required: - id - name - breed title: Pet type: object ``` Now we have a complete OpenAPI document that we can use to generate SDK clients for our API. However, the generated OpenAPI document does not contain descriptions or example values for the models. We can add these details to the Pydantic models to improve the generated OpenAPI document. ### Adding descriptions to Pydantic models Let's add docstrings to the `Pet` and `Owner` models to include additional information in the generated OpenAPI document. ```python import yaml from pydantic import BaseModel from pydantic.json_schema import models_json_schema class Pet(BaseModel): """ A Pet in the system. ID is unique. Can have multiple owners. """ id: int name: str breed: str class Owner(BaseModel): """ An Owner of Pets in the system. ID is unique. Can have multiple pets. """ id: int name: str pets: list[Pet] def print_json_schema(models): _, schemas = models_json_schema( [(model, "validation") for model in models], ref_template="#/components/schemas/{model}", ) openapi_schema = { "openapi": "3.1.0", "info": { "title": "Pet Sitter API", "version": "0.0.1", }, "components": { "schemas": schemas.get("$defs"), }, } print(yaml.dump(openapi_schema, sort_keys=False)) if __name__ == "__main__": print_json_schema([Pet, Owner]) ``` If we run `python models.py`, we see that our `Owner` schema now includes a description field, derived from the docstring we added to the `Owner` Pydantic model. ```yaml openapi: 3.1.0 info: title: Pet Sitter API version: 0.0.1 components: schemas: Owner: description: "An Owner of Pets in the system. ID is unique. Can have multiple pets." properties: id: title: Id type: integer name: title: Name type: string pets: items: $ref: "#/components/schemas/Pet" title: Pets type: array required: - id - name - pets title: Owner type: object Pet: description: "A Pet in the system. ID is unique. Can have multiple owners." properties: id: title: Id type: integer name: title: Name type: string breed: title: Breed type: string required: - id - name - breed title: Pet type: object ``` The `Pet` schema now also includes a description field, derived from the docstring we added to the `Pet` Pydantic model. ### Adding OpenAPI titles and descriptions to Pydantic fields Let's add titles and descriptions to the fields of the `Pet` and `Owner` models to include additional information in the generated OpenAPI document. We'll use the `Field` class from Pydantic to add descriptions to the fields. ```python import yaml from pydantic import BaseModel, Field from pydantic.json_schema import models_json_schema class Pet(BaseModel): """ A Pet in the system. ID is unique. Can have multiple owners. """ id: int = Field(..., title="Pet ID", description="The pet's unique identifier") name: str = Field(..., title="Pet Name", description="Name of the pet") breed: str = Field(..., title="Pet Breed", description="Breed of the pet") class Owner(BaseModel): """ An Owner of Pets in the system. ID is unique. Can have multiple pets. """ id: int = Field(..., title="Owner ID", description="Owner's unique identifier") name: str = Field(..., title="Owner Name", description="The owner's full name") pets: list[Pet] = Field( ..., title="Owner's Pets", description="The pets that belong to this owner" ) def print_json_schema(models): _, schemas = models_json_schema( [(model, "validation") for model in models], ref_template="#/components/schemas/{model}", ) openapi_schema = { "openapi": "3.1.0", "info": { "title": "Pet Sitter API", "version": "0.0.1", }, "components": { "schemas": schemas.get("$defs"), }, } print(yaml.dump(openapi_schema, sort_keys=False)) if __name__ == "__main__": print_json_schema([Pet, Owner]) ``` If we run `python models.py`, we see that our `Pet` schema now includes descriptions for each field. ```yaml openapi: 3.1.0 info: title: Pet Sitter API version: 0.0.1 components: schemas: Owner: description: "An Owner of Pets in the system. ID is unique. Can have multiple pets." properties: id: description: Owner's unique identifier title: Owner ID type: integer name: description: The owner's full name title: Owner Name type: string pets: description: The pets that belong to this owner items: $ref: "#/components/schemas/Pet" title: Owner's Pets type: array required: - id - name - pets title: Owner type: object Pet: description: "A Pet in the system. ID is unique. Can have multiple owners." properties: id: description: The pet's unique identifier title: Pet ID type: integer name: description: Name of the pet title: Pet Name type: string breed: description: Breed of the pet title: Pet Breed type: string required: - id - name - breed title: Pet type: object ``` ### Adding OpenAPI example values to Pydantic models Examples help API users understand your API's data structures, and some SDK and documentation generators use OpenAPI example values to generate useful code snippets and documentation. Let's add example values to the `Pet` and `Owner` Pydantic models. Once again, we'll use the `Field` class from Pydantic to add example values to the fields. Note that the examples are added as a list per field, using the `examples` parameter. ```python import yaml from pydantic import BaseModel, Field from pydantic.json_schema import models_json_schema class Pet(BaseModel): """ A Pet in the system. ID is unique. Can have multiple owners. """ id: int = Field( ..., title="Pet ID", description="The pet's unique identifier", examples=[1], ) name: str = Field( ..., title="Pet Name", description="Name of the pet", examples=["Fido"], ) breed: str = Field( ..., title="Pet Breed", description="Breed of the pet", examples=["Golden Retriever", "Siamese", "Parakeet"], ) class Owner(BaseModel): """ An Owner of Pets in the system. ID is unique. Can have multiple pets. """ id: int = Field( ..., title="Owner ID", description="Owner's unique identifier", examples=[1], ) name: str = Field( ..., title="Owner Name", description="The owner's full name", examples=["John Doe"], ) pets: list[Pet] = Field( ..., title="Owner's Pets", description="The pets that belong to this owner", examples=[{"id": 1}], ) def print_json_schema(models): _, schemas = models_json_schema( [(model, "validation") for model in models], ref_template="#/components/schemas/{model}", ) openapi_schema = { "openapi": "3.1.0", "info": { "title": "Pet Sitter API", "version": "0.0.1", }, "components": { "schemas": schemas.get("$defs"), }, } print(yaml.dump(openapi_schema, sort_keys=False)) if __name__ == "__main__": print_json_schema([Pet, Owner]) ``` If we run `python models.py`, we see that our `Pet` schema now includes example values for each field. ```yaml openapi: 3.1.0 info: title: Pet Sitter API version: 0.0.1 components: schemas: Owner: description: "An Owner of Pets in the system. ID is unique. Can have multiple pets." properties: id: description: Owner's unique identifier examples: - 1 title: Owner ID type: integer name: description: The owner's full name examples: - John Doe title: Owner Name type: string pets: description: The pets that belong to this owner examples: - id: 1 items: $ref: "#/components/schemas/Pet" title: Owner's Pets type: array required: - id - name - pets title: Owner type: object Pet: description: "A Pet in the system. ID is unique. Can have multiple owners." properties: id: description: The pet's unique identifier examples: - 1 title: Pet ID type: integer name: description: Name of the pet examples: - Fido title: Pet Name type: string breed: description: Breed of the pet examples: - Golden Retriever - Siamese - Parakeet title: Pet Breed type: string required: - id - name - breed title: Pet type: object ``` ### Marking fields as optional in Pydantic models By default, Pydantic marks all fields as required. You can mark a field as optional by setting the `default` parameter to `None`. Let's mark the `breed` field in the `Pet` model as optional by setting the `default` parameter to `None`. ```python import yaml from pydantic import BaseModel, Field from pydantic.json_schema import models_json_schema class Pet(BaseModel): """ A Pet in the system. ID is unique. Can have multiple owners. """ id: int = Field( ..., title="Pet ID", description="The pet's unique identifier", examples=[1], ) name: str = Field( ..., title="Pet Name", description="Name of the pet", examples=["Fido"], ) breed: str | None = Field( None, title="Pet Breed", description="Breed of the pet", examples=["Golden Retriever", "Siamese", "Parakeet"], ) class Owner(BaseModel): """ An Owner of Pets in the system. ID is unique. Can have multiple pets. """ id: int = Field( ..., title="Owner ID", description="Owner's unique identifier", examples=[1], ) name: str = Field( ..., title="Owner Name", description="The owner's full name", examples=["John Doe"], ) pets: list[Pet] = Field( ..., title="Owner's Pets", description="The pets that belong to this owner", examples=[{"id": 1}], ) def print_json_schema(models): _, schemas = models_json_schema( [(model, "validation") for model in models], ref_template="#/components/schemas/{model}", ) openapi_schema = { "openapi": "3.1.0", "info": { "title": "Pet Sitter API", "version": "0.0.1", }, "components": { "schemas": schemas.get("$defs"), }, } print(yaml.dump(openapi_schema, sort_keys=False)) if __name__ == "__main__": print_json_schema([Pet, Owner]) ``` If we run `python models.py`, we see that the `breed` field in the `Pet` schema now has two types: `string` and `null`, and it has been removed from the `required` list. Only `id` and `name` are required fields after marking `breed` as optional. ```yaml openapi: 3.1.0 info: title: Pet Sitter API version: 0.0.1 components: schemas: Owner: description: "An Owner of Pets in the system. ID is unique. Can have multiple pets." properties: id: description: Owner's unique identifier examples: - 1 title: Owner ID type: integer name: description: The owner's full name examples: - John Doe title: Owner Name type: string pets: description: The pets that belong to this owner examples: - id: 1 items: $ref: "#/components/schemas/Pet" title: Owner's Pets type: array required: - id - name - pets title: Owner type: object Pet: description: "A Pet in the system. ID is unique. Can have multiple owners." properties: id: description: The pet's unique identifier examples: - 1 title: Pet ID type: integer name: description: Name of the pet examples: - Fido title: Pet Name type: string breed: anyOf: - type: string - type: "null" default: null description: Breed of the pet examples: - Golden Retriever - Siamese - Parakeet title: Pet Breed required: - id - name title: Pet type: object ``` ### Adding enums to OpenAPI using Pydantic models Enums in OpenAPI are useful for defining a set of possible values for a field. Let's add an enum called `PetType` to the `Pet` model to represent different types of pets. ```python from enum import StrEnum import yaml from pydantic import BaseModel, Field from pydantic.json_schema import models_json_schema class PetType(StrEnum): """ An enumeration of pet types. """ DOG = "dog" CAT = "cat" BIRD = "bird" class Pet(BaseModel): """ A Pet in the system. ID is unique. Can have multiple owners. """ pet_type: PetType = Field( ..., title="Pet Type", description="Type of pet", examples=["dog", "cat", "bird"], ) id: int = Field( ..., title="Pet ID", description="The pet's unique identifier", examples=[1], ) name: str = Field( ..., title="Pet Name", description="Name of the pet", examples=["Fido"], ) breed: str | None = Field( None, title="Pet Breed", description="Breed of the pet", examples=["Golden Retriever", "Siamese", "Parakeet"], ) class Owner(BaseModel): """ An Owner of Pets in the system. ID is unique. Can have multiple pets. """ id: int = Field( ..., title="Owner ID", description="Owner's unique identifier", examples=[1], ) name: str = Field( ..., title="Owner Name", description="The owner's full name", examples=["John Doe"], ) pets: list[Pet] = Field( ..., title="Owner's Pets", description="The pets that belong to this owner", examples=[{"id": 1}], ) def print_json_schema(models): _, schemas = models_json_schema( [(model, "validation") for model in models], ref_template="#/components/schemas/{model}", ) openapi_schema = { "openapi": "3.1.0", "info": { "title": "Pet Sitter API", "version": "0.0.1", }, "components": { "schemas": schemas.get("$defs"), }, } print(yaml.dump(openapi_schema, sort_keys=False)) if __name__ == "__main__": print_json_schema([Pet, Owner]) ``` In our generated OpenAPI document, we have a new `pet_type` field in the `Pet` schema. ```yaml openapi: 3.1.0 info: title: Pet Sitter API version: 0.0.1 components: schemas: Owner: description: "An Owner of Pets in the system. ID is unique. Can have multiple pets." properties: id: description: Owner's unique identifier examples: - 1 title: Owner ID type: integer name: description: The owner's full name examples: - John Doe title: Owner Name type: string pets: description: The pets that belong to this owner examples: - id: 1 items: $ref: "#/components/schemas/Pet" title: Owner's Pets type: array required: - id - name - pets title: Owner type: object Pet: description: "A Pet in the system. ID is unique. Can have multiple owners." properties: pet_type: allOf: - $ref: "#/components/schemas/PetType" description: Type of pet examples: - dog - cat - bird title: Pet Type id: description: The pet's unique identifier examples: - 1 title: Pet ID type: integer name: description: Name of the pet examples: - Fido title: Pet Name type: string breed: anyOf: - type: string - type: "null" default: null description: Breed of the pet examples: - Golden Retriever - Siamese - Parakeet title: Pet Breed required: - pet_type - id - name title: Pet type: object PetType: description: An enumeration of pet types. enum: - dog - cat - bird title: PetType type: string ``` This enum is represented as a separate schema in the OpenAPI document. ## Adding paths and operations to the OpenAPI document Now that we have generated an OpenAPI document from our Pydantic models, we can use the schema to generate SDK clients for our API. However, the OpenAPI document we generated, while valid, does not include the `paths` section, which defines the API endpoints and operations. When using Pydantic with FastAPI, you can define your API endpoints and operations directly in your FastAPI application. [FastAPI automatically generates the OpenAPI document for your API](/openapi/frameworks/fastapi#speakeasy-integration), including the `paths` section. Let's see how we can define API endpoints and operations in a framework-agnostic way and add them to the OpenAPI document. ### Installing openapi-pydantic We'll use the [`openapi-pydantic`](https://github.com/mike-oakley/openapi-pydantic/) library to define a complete OpenAPI document with paths and operations. The benefit of using `openapi-pydantic` is that it allows you to define the API endpoints and operations in a Python dictionary while still getting the benefit of Pydantic's IDE support and type checking. The library includes convenience methods to convert Pydantic models to OpenAPI document components and to add them to the OpenAPI document. Install the `openapi-pydantic` library: ```bash filename="Terminal" pip install openapi-pydantic ``` ### Defining API endpoints Create a new file called `api.py` to define the API endpoints using the `openapi-pydantic` library: ```python filename="api.py" from typing import List import yaml from pydantic import BaseModel, Field from openapi_pydantic.v3 import OpenAPI, Info, PathItem, Operation from openapi_pydantic.util import PydanticSchema, construct_open_api_with_schema_class from models import Pet, Owner # Define response wrapper models class PetsResponse(BaseModel): """A response containing a list of pets""" pets: List[Pet] = Field(..., description="List of pets") class OwnersResponse(BaseModel): """A response containing a list of owners""" owners: List[Owner] = Field(..., description="List of owners") def construct_base_open_api() -> OpenAPI: return OpenAPI( openapi="3.1.0", info=Info(title="Pet Sitter API", version="0.0.1"), servers=[{"url": "http://127.0.0.1:4010", "description": "Local prism server"}], paths={ # GET and POST endpoints for pets collection "/pets": PathItem( get=Operation( operationId="listPets", description="List all pets", responses={ "200": { "description": "A list of pets", "content": { "application/json": { "schema": PydanticSchema(schema_class=PetsResponse) } }, } }, ), post=Operation( operationId="createPet", description="Create a pet", requestBody={ "content": { "application/json": {"schema": PydanticSchema(schema_class=Pet)} } }, responses={ "201": { "description": "Pet created", "content": { "application/json": {"schema": PydanticSchema(schema_class=Pet)} }, } }, ), ), # GET endpoint for a specific pet by ID "/pets/{pet_id}": PathItem( get=Operation( operationId="getPetById", description="Get a pet by ID", parameters=[ { "name": "pet_id", "in": "path", "description": "ID of pet to return", "required": True, "schema": {"type": "integer", "format": "int64"}, "examples": {"1": {"value": 1}}, }, ], responses={ "200": { "description": "A pet", "content": { "application/json": {"schema": PydanticSchema(schema_class=Pet)} }, } }, ), ), # GET endpoint for owners collection "/owners": PathItem( get=Operation( operationId="listOwners", description="List all owners", responses={ "200": { "description": "A list of owners", "content": { "application/json": { "schema": PydanticSchema(schema_class=OwnersResponse) } }, } }, ), ), }, ) # Generate the complete OpenAPI document open_api = construct_base_open_api() open_api = construct_open_api_with_schema_class(open_api) if __name__ == "__main__": with open("openapi.yaml", "w") as file: file.write( yaml.dump( open_api.model_dump( by_alias=True, mode="json", exclude_none=True, exclude_unset=True, ), sort_keys=False, ) ) ``` This code defines: 1. Response models that wrap our Pydantic models (like `PetsResponse`) for consistent API responses 2. A function that builds the OpenAPI document with four endpoints: - `GET /pets`: Lists all pets - `POST /pets`: Creates a new pet - `GET /pets/{pet_id}`: Gets a pet by ID - `GET /owners`: Lists all owners 3. Each endpoint includes: - An `operationId` for SDK generation - A description of what the endpoint does - Request parameters/body (where applicable) - Response schemas that reference our Pydantic models When run, this generates an `openapi.yaml` file with the full API specification: ```yaml filename="openapi.yaml" openapi: 3.1.0 info: title: Pet Sitter API version: 0.0.1 servers: - url: http://127.0.0.1:4010 description: Local prism server paths: /pets: get: description: List all pets operationId: listPets responses: "200": description: A list of pets content: application/json: schema: $ref: "#/components/schemas/PetsResponse" post: description: Create a pet operationId: createPet requestBody: content: application/json: schema: $ref: "#/components/schemas/Pet" responses: "201": description: Pet created content: application/json: schema: $ref: "#/components/schemas/Pet" /pets/{pet_id}: get: description: Get a pet by ID operationId: getPetById parameters: - name: pet_id in: path description: ID of pet to return required: true schema: type: integer format: int64 examples: "1": value: 1 responses: "200": description: A pet content: application/json: schema: $ref: "#/components/schemas/Pet" /owners: get: description: List all owners operationId: listOwners responses: "200": description: A list of owners content: application/json: schema: $ref: "#/components/schemas/OwnersResponse" components: schemas: # Schemas for our models are included here # (Pet, Owner, PetType, PetsResponse, OwnersResponse) ``` The generated OpenAPI document includes all the components from our Pydantic models, along with the API endpoints we defined. The schemas include all the titles, descriptions, examples, and other details we added to our Pydantic models. ## Generating an SDK from the OpenAPI document Now that we have a complete OpenAPI document with paths and operations, we can use it to generate an SDK client for our API. ### Prerequisites for SDK generation Install Speakeasy by following the [Speakeasy installation instructions](/docs/speakeasy-reference/cli/getting-started#install) On macOS, you can install Speakeasy using Homebrew: ```bash filename="Terminal" brew install speakeasy-api/tap/speakeasy ``` Authenticate with Speakeasy using the following command: ```bash filename="Terminal" speakeasy auth login ``` ### Generating an SDK using Speakeasy Run the following command to generate an SDK from the `openapi.yaml` file: ```bash filename="Terminal" speakeasy quickstart ``` Follow the onscreen prompts to provide the necessary configuration details for your new SDK, such as the name, schema location, and output path. Enter `openapi.yaml` when prompted for the OpenAPI document location and select TypeScript when prompted for which language you would like to generate. Speakeasy [validates](/docs/sdks/core-concepts#validation) the OpenAPI document to check that it's ready for code generation. Validation issues will be printed in the terminal. The generated SDK will be saved as a folder in your project. ![Speakeasy quickstart command output](/assets/openapi/speakeasy-quickstart-output.png) Speakeasy also suggests improvements for your SDK using [Speakeasy Suggest](/docs/prep-openapi/maintenance), which is an AI-powered tool in Speakeasy Studio. You can view the suggestions in Speakeasy Studio: ![Speakeasy Studio suggestions](/assets/openapi/hono/speakeasy-studio-suggestions.png) ### Adding Speakeasy extensions to the OpenAPI document Speakeasy uses [OpenAPI extensions](/openapi/extensions) to provide additional information for generating SDKs. We can add extensions using [OpenAPI overlays](/openapi/overlays), which are YAML files that [Speakeasy lays on top of the OpenAPI document](/docs/prep-openapi/overlays/create-overlays). We can use overlays alongside [OpenAPI transformations](/docs/prep-openapi/transformations) to improve the OpenAPI document for SDK generation. Transformations are predefined functions that allow you to remove unused components, filter operations, and format your OpenAPI document. Unlike overlays, transformations directly modify the OpenAPI document itself. Note that for Speakeasy OpenAPI extensions, you can also add extensions directly to the OpenAPI document using the `x-` prefix. For example, you can add the [`x-speakeasy-retries`](/docs/customize/runtime/retries) extension to have Speakeasy generate retry logic in the SDK. Import the `Dict` and `Any` types from the `typing` module in `api.py`, and `ConfigDict` from `pydantic`. We'll use these types to define the `x-speakeasy-retries` extension in the OpenAPI document. ```python filename="api.py" from typing import List, Dict, Any import yaml from pydantic import BaseModel, Field, ConfigDict from openapi_pydantic.v3 import OpenAPI, Info, PathItem, Operation from openapi_pydantic.util import PydanticSchema, construct_open_api_with_schema_class from models import Pet, Owner # Define response models class PetsResponse(BaseModel): """A response containing a list of pets""" pets: List[Pet] = Field(..., description="List of pets") class OwnersResponse(BaseModel): """A response containing a list of owners""" owners: List[Owner] = Field(..., description="List of owners") # Define OpenAPI class with retry extension class OpenAPIwithRetries(OpenAPI): """OpenAPI with x-speakeasy-retries extension""" xSpeakeasyRetries: Dict[str, Any] = Field( ..., description="Retry configuration for the API", alias="x-speakeasy-retries", ) model_config = ConfigDict(populate_by_name=True) def construct_base_open_api() -> OpenAPIwithRetries: return OpenAPIwithRetries( openapi="3.1.0", info=Info(title="Pet Sitter API", version="0.0.1"), servers=[{"url": "http://127.0.0.1:4010", "description": "Local prism server"}], # Add retry configuration xSpeakeasyRetries={ "strategy": "backoff", "backoff": { "initialInterval": 500, "maxInterval": 60000, "maxElapsedTime": 3600000, "exponent": 1.5, }, "statusCodes": ["5XX"], "retryConnectionErrors": True, }, # Define API paths (endpoints) paths={ # ... API endpoints defined the same as before ... } ) ``` This produces an OpenAPI document with retry functionality: ```yaml filename="openapi.yaml" x-speakeasy-retries: strategy: backoff backoff: initialInterval: 500 maxInterval: 60000 maxElapsedTime: 3600000 exponent: 1.5 statusCodes: - 5XX retryConnectionErrors: true ``` ### Adding tags to the OpenAPI document To group operations in the OpenAPI document, you can use tags. This also allows Speakeasy to structure the generated SDK code and documentation logically. Add a `tags` field to the `OpenAPIwithRetries` object, then add a `tags` field to each operation in the `construct_base_open_api` function: ```python filename="api.py" def construct_base_open_api() -> OpenAPIwithRetries: return OpenAPIwithRetries( # Basic API info openapi="3.1.0", info=Info(title="Pet Sitter API", version="0.0.1"), # Define tags for grouping operations tags=[ {"name": "pets", "description": "Operations about pets"}, {"name": "owners", "description": "Operations about owners"}, ], # API endpoints with tags applied paths={ "/pets": PathItem( get=Operation( operationId="listPets", tags=["pets"], # other properties... ), ), # other endpoints... }, ) ``` Run `python api.py` to update the `openapi.yaml` file with the `tags` field, then regenerate the SDK using Speakeasy. ```bash filename="Terminal" python api.py speakeasy quickstart ``` Speakeasy will detect the changes to your OpenAPI document, generate the SDK with the updated tags, and automatically increment the SDK's version number. Take a look at the generated SDK to see how Speakeasy groups operations by tags. In the SDK `README.md` file, you'll find documentation about your Speakeasy SDK. TypeScript SDKs generated with Speakeasy include an installable [Model Context Protocol (MCP) server](/docs/standalone-mcp/build-server) where the various SDK methods are exposed as tools that AI applications can invoke. Your SDK documentation includes instructions for installing the MCP server. Note that the SDK is not ready for production use. To get it production-ready, follow the steps outlined in your Speakeasy Studio workspace. ## How Speakeasy helps get your Pydantic models ready for SDK generation In this tutorial, we learned how to generate an OpenAPI document from Pydantic models and use it to generate an SDK client using Speakeasy. If you would like to discuss how to get your Pydantic models ready for SDK generation, give us feedback, or shoot the breeze about all things OpenAPI and SDKs, [join our Slack](https://go.speakeasy.com/slack). If you haven't already, take a look at our [blog](/blog) to learn more about API design, SDK generation, and our latest features, including: - [Native JSONL support in your SDKs](/blog/release-jsonl-support) - [Introducing comprehensive SDK testing](/blog/release-sdk-testing) - [Model Context Protocol: TypeScript SDKs for the agentic AI ecosystem](/blog/release-model-context-protocol) - [Python generation with async and Pydantic support](/blog/release-python) - [Choosing your Python REST API framework](/blog/choosing-your-framework-python) # How to generate OpenAPI documentation with Rswag for Ruby on Rails Source: https://speakeasy.com/openapi/frameworks/rails When building APIs in a Ruby on Rails application, "Convention over Configuration" is already embraced. Building RESTful routes is standard for most Rails developers, as is writing tests using Test Driven Development (TDD), but creating an OpenAPI document that accurately describes an API can be another matter. [The OpenAPI Specification](https://swagger.io/specification/) (formerly Swagger) has become the industry standard for documenting RESTful APIs, but manually writing and maintaining OpenAPI documents can be a time-consuming and error-prone process. [Rswag](https://github.com/rswag/rswag) solves this problem by enabling OpenAPI documents to be generated directly from RSpec tests. This approach helps documentation stay in sync with the actual API implementation. This guide demonstrates how to: - Document a Rails API with Rswag's RSpec-based domain-specific language (DSL). - Generate OpenAPI documents. - Use the Speakeasy CLI to generate client SDKs. The guide also covers customization of OpenAPI documents and troubleshooting common issues. ## What is Rswag? [Rswag](https://github.com/rswag/rswag) is a Ruby gem that helps generate documentation for Rails APIs. Unlike other Ruby gems that add API documentation directly into code with comments or special attributes, Rswag builds API documentation through tests. The idea behind Rswag is simple: RSpec tests describe API behavior using Rswag's extended rspec-rails OpenAPI-based DSL, and those tests are used to generate OpenAPI documentation. Unlike other tools, this approach tests the application while producing documentation at the same time. ### Wait, what's RSpec? [RSpec](https://rspec.info/) is a Ruby testing framework used to write tests for code. Specifically, [rspec-rails](https://github.com/rspec/rspec-rails) brings RSpec testing to Rails applications. RSpec has a DSL (Domain Specific Language) that makes tests easy to read and understand. Rswag builds on this with its OpenAPI-based DSL to describe API endpoints and then generate the OpenAPI document from spec tests. ### So, to summarize: ```mermaid graph TD R[(Rails API endpoints)] -->|are defined in| A[RSpec request specs] A -->|feed| B[Rswag generator] B --> |generate| C[(OpenAPI document)] ``` Rswag has three main parts used in this guide: - `rswag-specs`: Adds an OpenAPI-based DSL to [RSpec](https://rspec.info/) to describe API endpoints. When these spec tests run, they verify that the API works correctly and collect information for the documentation. - `rswag-api`: Creates an endpoint in the Rails app that serves the OpenAPI document (in JSON or YAML format) for use by other tools. - `rswag-ui`: Adds the OpenAPI documentation UI to the app, providing a webpage for viewing and trying out the API. Rswag keeps documentation aligned with code changes. If the API behavior changes without updated documentation, tests will fail, helping catch documentation errors early. Because the documentation is generated from tests that run against the API, the documented behavior reflects reality. ## Example API repository The source code for a complete implementation of this guide is available in the [rails-f1-laps-api repository](https://github.com/speakeasy-api/examples/tree/main/frameworks-rails-rswag). Clone the repository to follow along with the tutorial or use it as a reference for a Rails project. This guide uses a simple Formula 1 (F1) lap times API with the following resources: - **Drivers**: F1 drivers with their names, codes, and countries - **Circuits**: Racing circuits with names and locations - **Lap times**: Records of lap times for drivers on specific circuits The API allows clients to list all drivers, circuits, and lap times. Query parameters can be used to filter lap times by specific drivers, circuits, and lap numbers, and `POST` requests can be used to create new lap time records. ### Requirements To follow this guide, the following should be available: - Ruby on Rails installed - A Rails API application (the example app provided above can be used) ## Adding Rswag to a Rails application Begin by adding Rswag to a Rails application. First, add the Rswag gems to the application's Gemfile: ```ruby filename="Gemfile" group :development, :test do gem 'rswag-specs' end gem 'rswag-api' gem 'rswag-ui' ``` The `rswag-specs` gem is needed only for development and testing, while the `rswag-api` and `rswag-ui` gems are required in all environments to allow other tools to interact with OpenAPI specs. After updating the Gemfile, install the gems: ```bash bundle install ``` Now, run the Rswag generators to set up the necessary files: ```bash rails generate rswag:api:install rails generate rswag:ui:install rails generate rswag:specs:install ``` These generators create several important files, including: - `config/initializers/rswag_api.rb`, which configures how the Rails Enginge exposes the OpenAPI files - `config/initializers/rswag_ui.rb`, which configures the OpenAPI UI and OpenAPI endpoints - `spec/swagger_helper.rb`, which sets up RSpec for generating OpenAPI specifications - `config/routes.rb`, configures where Rails mounts Rswag's OpenAPI documentation engine ## Configuring Rswag With the Rswag components installed, configure them to work with the API. ### Configuring the OpenAPI document generator The `spec/swagger_helper.rb` file is the central configuration for API documentation. Here is an example configuration for the F1 Laps API: ```ruby filename="swagger_helper.rb" RSpec.configure do |config| config.openapi_root = Rails.root.to_s + '/openapi' config.openapi_specs = { 'v1/openapi.yaml' => { openapi: '3.0.1', info: { title: 'F1 Laps API', version: 'v1', description: 'API for accessing Formula 1 lap time data and analytics', contact: { name: 'API Support', email: 'support@f1laps.com' }, license: { name: 'MIT', url: 'https://opensource.org/licenses/MIT' } }, paths: {}, components: { securitySchemes: { bearer_auth: { type: :http, scheme: :bearer, bearerFormat: 'JWT' } } }, servers: [ { url: 'http://{defaultHost}', variables: { defaultHost: { default: 'localhost:3000' } } } ] } } config.openapi_format = :yaml end ``` This configuration defines: - The location where the OpenAPI files will be generated (`openapi_root`) - The specification title, version, and description - Contact information and license details - Security schemes for authentication, such as JWT bearer tokens - Server information ### Configuring the OpenAPI UI The OpenAPI UI can be customized through the `config/initializers/rswag_ui.rb` file, which renders at `<api-url>/api-docs` by default: ```ruby filename="rswag_ui.rb" Rswag::Ui.configure do |c| c.openapi_endpoint '/api-docs/v1/openapi.yaml', 'F1 Laps API V1' # UI configuration options c.config_object['defaultModelsExpandDepth'] = 2 c.config_object['defaultModelExpandDepth'] = 2 c.config_object['defaultModelRendering'] = 'model' c.config_object['displayRequestDuration'] = true c.config_object['docExpansion'] = 'list' c.config_object['filter'] = true c.config_object['showExtensions'] = true c.config_object['showCommonExtensions'] = true c.config_object['tryItOutEnabled'] = true end ``` ### Configuring the OpenAPI files (rswag_api.rb) The `config/initializers/rswag_api.rb` file configures the root location where the OpenAPI files are served: ```ruby filename="rswag_api.rb" Rswag::Api.configure do |c| c.openapi_root = Rails.root.to_s + '/openapi' end ``` When using rswag-specs to generate OpenAPI files, ensure both `rswag-api` and `swagger_helper.rb` use the same `<openapi_root>`. Different settings exist to support setups where `rswag-api` is installed independently and OpenAPI files are created manually. ## Writing Rswag documentation specs The most powerful feature of Rswag is the ability to generate OpenAPI documentation directly from RSpec tests. These tests not only verify API functionality but also produce detailed OpenAPI documentation. The following sections show how to write these OpenAPI documents for different endpoints. ### Documenting a simple endpoint Let's start with a simple health check endpoint that returns basic API status information: ```ruby filename="health_spec.rb" # spec/requests/api/v1/health_spec.rb require 'swagger_helper' RSpec.describe 'Health API', type: :request do path '/api/v1/health' do get 'Get API health status' do tags 'Health' produces 'application/json' response '200', 'health status' do schema type: :object, properties: { status: { type: :string, enum: ['healthy'] }, version: { type: :string }, timestamp: { type: :string, format: 'date-time' } }, required: ['status', 'version', 'timestamp'] run_test! end end end end ``` This spec does a few things: - Defines the `/api/v1/health` endpoint as a `GET` request - Categorizes it under the `'Health'` tag for organization - Specifies that it produces JSON responses - Documents the expected `200` response with a detailed schema - Uses `run_test!` to execute the test and validate the actual response The `run_test!` method makes a request to the API and verifies that the response matches the documented schema, helping documentation remain accurate and aligned with the implementation. ### Documenting endpoints with parameters For more complex endpoints, such as those with parameters, request bodies, and multiple response types, more detailed specs can be created: ```ruby filename="lap_times_spec.rb" # spec/requests/api/v1/lap_times_spec.rb require 'swagger_helper' RSpec.describe 'Lap Times API', type: :request do path '/api/v1/lap_times' do get 'List all lap times' do tags 'Lap Times' produces 'application/json' parameter name: :driver_id, in: :query, type: :integer, required: false, description: 'Filter by driver ID' parameter name: :circuit_id, in: :query, type: :integer, required: false, description: 'Filter by circuit ID' parameter name: :lap_min, in: :query, type: :integer, required: false, description: 'Minimum lap number' parameter name: :lap_max, in: :query, type: :integer, required: false, description: 'Maximum lap number' response '200', 'lap times found' do schema type: :array, items: { type: :object, properties: { id: { type: :integer }, driver_id: { type: :integer }, circuit_id: { type: :integer }, time_ms: { type: :integer }, lap_number: { type: :integer }, created_at: { type: :string, format: 'date-time' }, updated_at: { type: :string, format: 'date-time' } }, required: ['id', 'driver_id', 'circuit_id', 'time_ms', 'lap_number'] } run_test! end end post 'Create a lap time' do tags 'Lap Times' consumes 'application/json' produces 'application/json' parameter name: :lap_time, in: :body, schema: { type: :object, properties: { driver_id: { type: :integer }, circuit_id: { type: :integer }, time_ms: { type: :integer }, lap_number: { type: :integer } }, required: ['driver_id', 'circuit_id', 'time_ms', 'lap_number'] } response '201', 'lap time created' do let(:lap_time) { { driver_id: 1, circuit_id: 1, time_ms: 80000, lap_number: 1 } } run_test! end response '422', 'invalid request' do let(:lap_time) { { driver_id: 1 } } run_test! end end end # Document nested routes path '/api/v1/drivers/{driver_id}/lap_times' do get 'Get lap times for a specific driver' do tags 'Lap Times' produces 'application/json' parameter name: :driver_id, in: :path, type: :integer, required: true response '200', 'lap times found' do let(:driver_id) { 1 } schema type: :array, items: { type: :object, properties: { id: { type: :integer }, circuit_id: { type: :integer }, time_ms: { type: :integer }, lap_number: { type: :integer }, created_at: { type: :string, format: 'date-time' }, updated_at: { type: :string, format: 'date-time' } } } run_test! end end end end ``` This more detailed spec documents multiple HTTP methods, query parameters, request bodies, different response types, and nested routes. The `let` statements provide test data that will be used when executing the tests. ### Understanding the Rswag DSL When writing Rswag documentation specs, the following elements are used to describe the API: - **Path and HTTP method definitions:** The `path` method defines the API endpoint being documented. ```ruby drivers_spec.rb path '/api/v1/drivers' do get 'List all drivers' do # Documentation for GET request end end ``` - **Tags for organization:** Tags help group related operations together, making your documentation more organized. ```ruby path '/api/v1/drivers' do get 'List all drivers' do tags 'Drivers' # Other documentation end end ``` - **Content types:** Specify what your API consumes and produces. ```ruby drivers_spec.rb path '/api/v1/drivers' do get 'List all drivers' do tags 'Drivers' produces 'application/json' consumes 'application/json' # Other documentation end end ``` - **Document parameters:** Define the query, path, or body parameters. ```ruby drivers_spec.rb path '/api/v1/drivers' do get 'List all drivers' do tags 'Drivers' produces 'application/json' parameter name: :team, in: :query, type: :string, required: false, description: 'Filter drivers by team' # Other documentation end end ``` - **Response definitions:** Define the possible responses with their schemas. ```ruby drivers_spec.rb path '/api/v1/drivers' do get 'List all drivers' do tags 'Drivers' produces 'application/json' parameter name: :team, in: :query, type: :string, required: false, description: 'Filter drivers by team' response '200', 'drivers found' do schema type: :array, items: { type: :object, properties: { id: { type: :integer }, name: { type: :string }, code: { type: :string } } } run_test! end end end ``` - **Test data:** Provide test data by using the `let` syntax to define the values that will be used during testing. ```ruby filename="drivers_spec.rb" path '/api/v1/drivers' do post 'Create a driver' do # ... parameter and other definitions ... response '201', 'driver created' do let(:driver) { { name: 'Max Verstappen', code: 'VER' } } run_test! end end end ``` ## Generating the OpenAPI document After writing documentation specs, generate the OpenAPI document by running a single `rake` task: ```bash rake rswag:specs:swaggerize ``` Alternatively, run the aliased command: ```bash rake rswag ``` If the command fails, set the environment to "test" using: ```bash RAILS_ENV=test rails rswag ``` This command performs two important steps: - Runs Rswag specs to validate that the API implementation matches the documentation. - It generates the OpenAPI document file at the configured location. The result is an OpenAPI document (for example, `openapi/v1/openapi.yaml`) usable with various tools, including the built-in OpenAPI UI and the Speakeasy CLI. If tests fail during this process, it indicates that the API implementation doesn't match the documentation. This behavior ensures documentation stays accurate and aligned with the actual implementation. ### Understanding the generated OpenAPI document After running the `rswag:specs:swaggerize` command, Rswag generates a comprehensive OpenAPI document. Here's what a section of that generated document looks like for the lap times endpoint: ```yaml filename="openapi.yaml" # Generated OpenAPI spec for Lap Times endpoint "/api/v1/lap_times": get: summary: List all lap times tags: - Lap Times parameters: - name: driver_id in: query required: false description: Filter by driver ID schema: type: integer - name: circuit_id in: query required: false description: Filter by circuit ID schema: type: integer - name: lap_min in: query required: false description: Minimum lap number schema: type: integer - name: lap_max in: query required: false description: Maximum lap number schema: type: integer responses: "200": description: lap times found content: application/json: schema: type: array items: type: object properties: id: type: integer driver_id: type: integer circuit_id: type: integer time_ms: type: integer lap_number: type: integer created_at: type: string format: date-time updated_at: type: string format: date-time required: - id - driver_id - circuit_id - time_ms - lap_number post: summary: Create a lap time tags: - Lap Times parameters: [] responses: "201": description: lap time created "422": description: invalid request requestBody: content: application/json: schema: type: object properties: driver_id: type: integer circuit_id: type: integer time_ms: type: integer lap_number: type: integer required: - driver_id - circuit_id - time_ms - lap_number ``` Rswag automatically documents: - The HTTP methods (`GET` and `POST`) - Query parameters for filtering - A request body schema for creating new records - Response codes and schemas - The required fields All of this is generated from Rswag spec files and matches the actual implementation of the API. ## Customizing the OpenAPI document While the basic Rswag setup provides a solid foundation, OpenAPI documents can be customized and enhanced with additional details to make them more useful to API consumers. ### Documenting authentication If the API requires authentication, configure security schemes in `spec/swagger_helper.rb` and add security requirements to specs: ```ruby filename="swagger_helper.rb" components: { securitySchemes: { bearer_auth: { type: :http, scheme: :bearer, bearerFormat: 'JWT' } } } ``` And then in the specs: ```ruby filename="lap_times_spec.rb" path '/api/v1/protected_resource' do get 'Access protected resource' do tags 'Protected' security [bearer_auth: []] # Other documentation end end ``` This tells API consumers that they need to include a bearer token in their requests to access the protected endpoints. ### Documenting file uploads For endpoints that handle file uploads, use the `multipart/form-data` content type and specify file parameters: ```ruby filename="lap_times_spec.rb" post 'Upload file' do consumes 'multipart/form-data' parameter name: :file, in: :formData, type: :file, required: true response '200', 'file uploaded' do # Test implementation end end ``` ### Creating reusable schemas To keep specs DRY [(Don't Repeat Yourself)](https://en.wikipedia.org/wiki/Don%27t_repeat_yourself), define reusable schema components in `spec/swagger_helper.rb`: ```ruby filename="swagger_helper.rb" components: { schemas: { lap_time: { type: :object, properties: { driver_id: { type: :integer }, circuit_id: { type: :integer }, time_ms: { type: :integer }, lap_number: { type: :integer } }, required: ['driver_id', 'circuit_id', 'time_ms', 'lap_number'] } } } ``` And then in the specs: ```ruby filename="lap_times_spec.rb" parameter name: :lap_time, in: :body, schema: { '$ref' => '#/components/schemas/lap_time' } ``` This allows common models to be defined once and referenced throughout the documentation. ## Troubleshooting common issues Common issues encountered when using Rswag and how to troubleshoot them are outlined below. ### Missing documentation If endpoints do not appear in the OpenAPI UI: - Ensure specs include the proper Rswag DSL syntax. - Verify that spec files are in the correct location. - Check that controller routes match the paths in the specs. Ideally, the CLI provides a helpful error message if something is missing. ### Test failures If Rswag specs fail, the implementation may not match the documentation. To fix this, check the generated OpenAPI document to identify missing elements, then update specs to match the implementation. Make sure to check the required parameters in both specs and controllers. ### Generation issues If the OpenAPI document has not been generated correctly, ensure the command is run in the test environment (`RAILS_ENV=test`) and that file permissions are correct in the destination directory. ## Generating SDKs with Speakeasy Once an OpenAPI document has been created with Rswag, Speakeasy can be used to generate client SDKs for the API. This makes it easier for developers to interact with the API in their preferred programming language. First, install the Speakeasy CLI: ```bash curl -fsSL https://go.speakeasy.com/cli-install.sh | sh ``` Next, follow the instructions on the [Getting Started](https://www.speakeasy.com/docs/speakeasy-reference/cli/getting-started) page to set up and authenticate with Speakeasy. To generate a client SDK, run the following command from the root of the project: ```bash speakeasy quickstart ``` Follow the prompts to provide the OpenAPI document location (`openapi/v1/openapi.yaml`) and configure SDK options. Speakeasy then generates a complete SDK based on the API specification, making it easier for developers to integrate with the API. ## Summary This guide explored how Rswag can be used to generate OpenAPI documents for a Rails API. It covered documenting API endpoints using Rswag's RSpec-based DSL, generating an OpenAPI document with Rswag, customizing the OpenAPI document, and using it to generate client SDKs with Speakeasy. # Generating an OpenAPI document and SDK from Spring Boot Source: https://speakeasy.com/openapi/frameworks/springboot import { Callout } from "@/mdx/components"; You have a Spring Boot API and need to generate SDKs or API documentation for other teams. Rather than writing and maintaining separate OpenAPI documents, we will walk through how to generate them directly from your Spring Boot code and then use them to create and customize an SDK. We'll work with real code you can run locally, building a simple bookstore API to demonstrate how to properly document API structures, including inheritance between models, endpoint definitions, response types, and error handling. The examples illustrate how Spring Boot annotations map to OpenAPI concepts, so you can see how your code translates into API specifications. <Callout title="Example repository" type="info"> The example below will guide you through the process of creating a Spring Boot project, adding the necessary dependencies, writing Spring Boot controllers with OpenAPI annotations, and generating an OpenAPI document from it. To skip this setup and follow along with our example, clone the [example application](https://github.com/speakeasy-api/examples/tree/main/framework-springboot). The example uses Java 21. </Callout> ## Setting up a Spring Boot project First, create a new Spring Boot project using [Spring Initializr](https://start.spring.io/). Select the following options: - Project: Maven - Language: Java - Spring Boot: 3.5.x (or the latest stable version) - Project Metadata: Fill in as appropriate - Dependencies: Spring Web Download the project and extract it to your preferred directory. ## Adding OpenAPI dependencies Open the `pom.xml` file and add the following dependency: ```xml filename="pom.xml" <dependency> <groupId>org.springdoc</groupId> <artifactId>springdoc-openapi-starter-webmvc-ui</artifactId> <version>2.8.8</version> </dependency> ``` ## Configuring application properties Open the `src/main/resources/application.properties` file and add the following configuration: ```properties filename="application.properties" # Specify the path of the OpenAPI documentation springdoc.api-docs.path=/api-docs springdoc.api-docs.version=OPENAPI_3_1 # Specify the path of the Swagger UI springdoc.swagger-ui.path=/swagger-ui.html ``` These properties configure the application name that identifies your service, the endpoint where the OpenAPI document will be available (`/api-docs`), the version of the OpenAPI document to generate, and the URL path where you can access the Swagger UI documentation (`/swagger-ui.html`). After starting your application, you can view the OpenAPI document at `http://localhost:8080/api-docs` and access the interactive Swagger UI at `http://localhost:8080/swagger-ui.html`. ## Writing a Spring Boot application You can find all the code for this step in the example application. Open the `src/main/java/com/bookstore/BookstoreApplication.java` file in your text editor to see where to begin when adding OpenAPI annotations to your project. ### Defining the main application configuration with annotations The `BookstoreApplication` class is the entry point for the API, and it's also where we define the main OpenAPI documentation properties: ```java filename="BookstoreApplication.java" package com.bookstore; import io.swagger.v3.oas.annotations.OpenAPIDefinition; import io.swagger.v3.oas.annotations.info.Contact; import io.swagger.v3.oas.annotations.info.Info; import io.swagger.v3.oas.annotations.info.License; import io.swagger.v3.oas.annotations.servers.Server; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; @SpringBootApplication @OpenAPIDefinition( info = @Info( title = "Bookstore API", version = "1.0.0", description = "This API provides endpoints to manage a bookstore's inventory of books and magazines, " + "as well as customer orders. You can use it to browse publications, create orders, and track " + "order status.", contact = @Contact( name = "Bookstore API Support", email = "api@bookstore.example.com", url = "https://bookstore.example.com/support" ), license = @License( name = "Apache 2.0", url = "https://www.apache.org/licenses/LICENSE-2.0.html" ) ), servers = { @Server(url = "https://api.bookstore.example.com", description = "Production server (uses live data)"), @Server(url = "http://localhost:8080", description = "Development server (uses test data)") } ) public class BookstoreApplication { public static void main(String[] args) { SpringApplication.run(BookstoreApplication.class, args); } } ``` The `@OpenAPIDefinition` annotation populates the OpenAPI document with essential context for anyone who wants to use the API. The `title`, `version`, and `description` fields describe what the API does, its current state, and how it can be used. The `@Server` annotation defines available endpoints for the API. In the example, there are two options: - A production server at `https://api.bookstore.example.com` that uses live data - A localhost server at `http://localhost:8080` for testing with sample data ### Defining data models with annotations Next, let's look at how you can use OpenAPI annotations to describe API data structures in the `Models.java` file: ```java filename="Models.java" package com.bookstore; import com.fasterxml.jackson.annotation.JsonIgnore; import com.fasterxml.jackson.annotation.JsonProperty; import com.fasterxml.jackson.annotation.JsonSubTypes; import com.fasterxml.jackson.annotation.JsonTypeInfo; import io.swagger.v3.oas.annotations.media.Schema; import java.time.LocalDate; import java.time.format.DateTimeFormatter; import java.util.List; @Schema(description = "Base class for all publications") @JsonTypeInfo( use = JsonTypeInfo.Id.NAME, property = "type", visible = true ) @JsonSubTypes({ @JsonSubTypes.Type(value = Book.class, name = "BOOK"), @JsonSubTypes.Type(value = Magazine.class, name = "MAGAZINE") }) public abstract class Publication { @Schema(description = "Unique identifier of the publication", example = "123e4567-e89b-12d3-a456-426614174000") protected String id; @Schema(description = "Title of the publication", example = "Spring Boot in Action") protected String title; @Schema(description = "Publication date in ISO format", example = "2023-05-15") protected String publicationDate; @Schema(description = "Price in USD", example = "29.99") protected float price; @JsonProperty("type") @Schema(description = "Type of publication", example = "BOOK", allowableValues = {"BOOK", "MAGAZINE"}) protected abstract String getType(); // Getters and setters omitted for brevity } @Schema(description = "Book publication with author and ISBN") class Book extends Publication { @Schema(description = "Author of the book", example = "Craig Walls") private String author; @Schema(description = "ISBN of the book", example = "978-1617292545") private String isbn; @Override @JsonIgnore protected String getType() { return "BOOK"; } // Constructor, getters, and setters omitted for brevity } @Schema(description = "Magazine publication with issue number and publisher") class Magazine extends Publication { @Schema(description = "Issue number of the magazine", example = "42") private int issueNumber; @Schema(description = "Publisher of the magazine", example = "O'Reilly Media") private String publisher; @Override @JsonIgnore protected String getType() { return "MAGAZINE"; } // Constructor, getters, and setters omitted for brevity } ``` The `@Schema` annotation can be used at both the class and field levels: - At the class level, `@Schema` describes what a `Publication`, `Book`, or `Magazine` represents in the API. - At the field level, fields like `id` and `author` are documented with a description and example values. The `Publication` class acts as the base schema in the OpenAPI specification. By using `@JsonTypeInfo` and `@JsonSubTypes`, we tell OpenAPI that a `Publication` can be either a `Book` or `Magazine`. This polymorphism is reflected in the OpenAPI document as a `oneOf` schema, allowing endpoints to accept or return either type. API clients will include a `type` field set to either `BOOK` or `MAGAZINE` to identify the publication type. Here's how we define an `Order` class that references the `Publication` schema: ```java filename="Models.java" @Schema(description = "Customer order for publications") class Order { @Schema(description = "Unique identifier of the order", example = "order-123456") private String id; @Schema(description = "Name of the customer who placed the order", example = "John Doe") private String customerName; @Schema(description = "Email of the customer", example = "john.doe@example.com") private String customerEmail; @Schema(description = "List of publications in the order") private List<Publication> items; @Schema(description = "Current status of the order", example = "PENDING") private OrderStatus status; // Constructor, getters, and setters omitted for brevity } ``` The `Order` class uses the `@Schema` annotation to document the `items` field, which references the `Publication` schema. This tells OpenAPI that `Orders` can contain an array of either books or magazines, using the polymorphic structure defined earlier. For the order status, we use an enumeration: ```java filename="Models.java" @Schema(description = "Status of an order") enum OrderStatus { PENDING, PROCESSING, SHIPPED, DELIVERED, CANCELLED } ``` This appears in the OpenAPI document as a string field with a set of allowed values. Finally, we define an error response model: ```java filename="Models.java" @Schema(description = "Error response with code and message") class ErrorResponse { @Schema(description = "Error code", example = "NOT_FOUND") private String code; @Schema(description = "Error message", example = "Publication with ID 123 not found") private String message; // Constructor, getters, and setters omitted for brevity } ``` ### Defining API endpoints with annotations Now, let's define the API endpoints in the `PublicationsController.java` file: ```java filename="PublicationsController.java" package com.bookstore; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Parameter; import io.swagger.v3.oas.annotations.media.ArraySchema; import io.swagger.v3.oas.annotations.media.Content; import io.swagger.v3.oas.annotations.media.Schema; import io.swagger.v3.oas.annotations.responses.ApiResponse; import io.swagger.v3.oas.annotations.responses.ApiResponses; import io.swagger.v3.oas.annotations.tags.Tag; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.*; import java.util.ArrayList; import java.util.List; import java.util.UUID; @RestController @RequestMapping("/publications") @Tag(name = "Publications", description = "Operations for managing publications (books and magazines)") public class PublicationsController { // Controller methods follow } ``` The `@Tag` annotation groups operations under "Publications" in the OpenAPI document. Combined with `@RequestMapping("/publications")`, it tells API consumers that these endpoints handle publication-related operations. For each endpoint method, we use annotations to document their purpose and responses: ```java filename="PublicationsController.java" @Operation(summary = "Get a publication by ID", description = "Returns a single publication (book or magazine)") @ApiResponses(value = { @ApiResponse(responseCode = "200", description = "Successful operation", content = @Content(schema = @Schema(oneOf = {Book.class, Magazine.class}))), @ApiResponse(responseCode = "404", description = "Publication not found", content = @Content(schema = @Schema(implementation = ErrorResponse.class))) }) @GetMapping("/{id}") public ResponseEntity<?> getPublication( @Parameter(description = "ID of the publication to return", required = true) @PathVariable String id) { // Implementation omitted for brevity } ``` The `@Operation` and `@ApiResponses` annotations document what the endpoint does and what responses to expect. For example, `getPublication` is annotated to show that it returns a publication successfully (`200` status) or returns an error (`404` status) when the publication isn't found. The `@Parameter` annotation describes the requirements for input parameters, such as the ID path parameter in this example. ## Examining the generated OpenAPI document Now that we've built the Spring Boot application, let's generate and examine the OpenAPI document to understand how the Java code translates into API specifications. First, install the necessary dependencies in the project and start the application with the following commands: ```bash filename="Terminal" ./mvnw clean install ./mvnw spring-boot:run ``` Download the OpenAPI document while running the application: ```bash filename="Terminal" curl http://localhost:8080/api-docs.yaml -o openapi.yaml ``` This command saves the OpenAPI document as `openapi.yaml` in your current directory. Let's explore the generated OpenAPI document to see how the Spring Boot annotations translate into an OpenAPI specification. ### The OpenAPI Specification version information The OpenAPI document begins with version information: ```yaml filename="openapi.yaml" openapi: 3.1.0 ``` This version is determined by the configuration in our `application.properties` file. It tells API consumers which version of the OpenAPI Specification to expect. ### API information Next comes the `info` object, which is generated from the `@OpenAPIDefinition` annotation: ```yaml filename="openapi.yaml" info: title: Bookstore API description: >- This API provides endpoints to manage a bookstore's inventory of books and magazines, as well as customer orders. You can use it to browse publications, create orders, and track order status. contact: name: Bookstore API Support url: https://bookstore.example.com/support email: api@bookstore.example.com license: name: Apache 2.0 url: https://www.apache.org/licenses/LICENSE-2.0.html version: 1.0.0 ``` Notice how each field in the Java annotation maps directly to its counterpart in the OpenAPI document output. This one-to-one mapping makes it easy to understand how your code affects the final API documentation. ### Server information Server configurations defined with `@Server` annotations appear in the servers array: ```yaml filename="openapi.yaml" servers: - url: https://api.bookstore.example.com description: Production server (uses live data) - url: http://localhost:8080 description: Development server (uses test data) ``` ### Polymorphic models One of the more complex aspects of the API is how polymorphic models are represented. The `Publication` class has been translated into a schema that supports polymorphism through a discriminator: ```yaml filename="openapi.yaml" Publication: type: object description: Base class for all publications properties: id: type: string description: Unique identifier of the publication example: 123e4567-e89b-12d3-a456-426614174000 title: type: string description: Title of the publication example: Spring Boot in Action publicationDate: type: string description: Publication date in ISO format example: "2023-05-15" price: type: number format: float description: Price in USD example: 29.99 type: type: string description: Type of publication example: BOOK enum: - BOOK - MAGAZINE required: - id - title - publicationDate - price - type discriminator: propertyName: type mapping: BOOK: "#/components/schemas/Book" MAGAZINE: "#/components/schemas/Magazine" ``` Key aspects to notice: - The `@Schema` annotations provide descriptions and examples - The `@JsonTypeInfo` annotation determines the discriminator property - The `@JsonSubTypes` annotation defines the possible concrete implementations ### API endpoints Finally, let's examine how controller methods translate into API endpoints. Here's how the `getPublication` endpoint appears in the OpenAPI document: ```yaml filename="openapi.yaml" /publications/{id}: get: tags: - Publications summary: Get a publication by ID description: Returns a single publication (book or magazine) operationId: getPublication parameters: - name: id in: path description: ID of the publication to return required: true schema: type: string responses: "200": description: Successful operation content: application/json: schema: type: object oneOf: - $ref: "#/components/schemas/Book" - $ref: "#/components/schemas/Magazine" "404": description: Publication not found content: application/json: schema: $ref: "#/components/schemas/ErrorResponse" ``` The mapping is clear: - The `@Operation` annotation provides the summary and description. - Each `@ApiResponse` maps to an entry in the responses object. - The `@Parameter` annotation documents the path parameter. ## Creating an SDK from the OpenAPI document Now that we have an OpenAPI document for the Spring Boot API, we can create an SDK using Speakeasy. First, make sure you have Speakeasy installed: ```bash filename="Terminal" speakeasy --version ``` Now, generate a TypeScript SDK using the following command: ```bash filename="Terminal" speakeasy quickstart ``` Follow the onscreen prompts to provide the configuration details for the new SDK, such as the name, schema location, and output path. Enter `openapi.yaml` when prompted for the OpenAPI document location and select your preferred language (for example, TypeScript) when prompted for which language you would like to generate. You'll see the steps taken by Speakeasy to create the SDK in the terminal: ```bash filename="Terminal" │ Workflow - running │ Workflow - success │ └─Target: sdk - success │ └─Source: Bookstore API - success │ └─Validating Document - success │ └─Diagnosing OpenAPI - success │ └─Tracking OpenAPI Changes - success │ └─Snapshotting OpenAPI Revision - success │ └─Storing OpenAPI Revision - success │ └─Validating gen.yaml - success │ └─Generating Typescript SDK - success │ └─Setup Environment - success │ └─Load and Validate Document - success │ └─Generate SDK - success │ └─Compile SDK - success │ └─Setup Environment - success │ └─Load and Validate Document - success │ └─Generate SDK - success │ └─Generating Code Samples - success │ └─Snapshotting Code Samples - success │ └─Snapshotting Code Samples - success │ └─Uploading Code Samples - success ``` Speakeasy [validates](/docs/sdks/core-concepts#validation) the OpenAPI document to check that it's ready for code generation. Validation issues will be printed in the terminal. The generated SDK will be saved as a folder in your project. If you get ESLint styling errors, run the `speakeasy quickstart` command from outside your project. Speakeasy also suggests improvements for your SDK using [Speakeasy Suggest](/docs/prep-openapi/maintenance), which is an AI-powered tool in Speakeasy Studio. You can see suggestions by opening the link to your Speakeasy Studio workspace in the terminal: ![Speakeasy Suggestions in Speakeasy Studio](/assets/openapi/springboot/speakeasy-suggestions.png) After running this command, you'll find the generated SDK code in the specified output directory. This SDK can be used by clients to interact with your Spring Boot API in a type safe manner. In the SDK `README.md` file, you'll find documentation about your Speakeasy SDK. TypeScript SDKs generated with Speakeasy include an installable [Model Context Protocol (MCP) server](/docs/standalone-mcp/build-server) where the various SDK methods are exposed as tools that AI applications can invoke. Your SDK documentation includes instructions for installing the MCP server. Note that the SDK is not ready for production use. To get it production-ready, follow the steps outlined in your Speakeasy workspace. ## Customizing the SDK The example app added retry logic to the SDK's `listPublications` operation to handle network errors gracefully. This was done using one of [Speakeasy's OpenAPI extensions](/docs/speakeasy-reference/extensions), `x-speakeasy-retries`. Instead of modifying the OpenAPI document directly, this extension was added to the Spring Boot controller, and the OpenAPI document and SDK were regenerated. These imports were added to `src/main/java/com/bookstore/PublicationsController.java`: ```java filename="PublicationsController.java" import io.swagger.v3.oas.annotations.extensions.Extension; import io.swagger.v3.oas.annotations.extensions.ExtensionProperty; ``` The `listPublications` operation was modified to include the retry configuration: ```java filename="PublicationsController.java" mark=2:7 @Operation(summary = "List all publications", description = "Get a list of all publications in the store", extensions = { @Extension(name = "x-speakeasy-retries", properties = { @ExtensionProperty(name = "strategy", value = "backoff"), @ExtensionProperty(name = "backoff", parseValue = true, value = "{\"initialInterval\":500,\"maxInterval\":60000,\"maxElapsedTime\":3600000,\"exponent\":1.5}"), @ExtensionProperty(name = "statusCodes", parseValue = true, value = "[\"5XX\"]"), @ExtensionProperty(name = "retryConnectionErrors", parseValue = true, value = "true") }) }) @ApiResponses(value = { @ApiResponse(responseCode = "200", description = "Successful operation", content = @Content(array = @ArraySchema(schema = @Schema(implementation = PublicationListItem.class)))), }) @GetMapping public ResponseEntity<List<Publication>> listPublications() { // This is a mock implementation. In a real application, you would fetch this from a database. List<Publication> publications = new ArrayList<>(); publications.add(new Book(UUID.randomUUID().toString(), "Spring Boot in Action", "2015-10-01", 39.99f, "Craig Walls", "978-1617292545")); publications.add(new Magazine(UUID.randomUUID().toString(), "National Geographic", "2023-06-01", 9.99f, 6, "National Geographic Society")); return ResponseEntity.ok(publications); } ``` The OpenAPI document was then regenerated: ```bash filename="Terminal" curl http://localhost:8080/api-docs.yaml -o openapi.yaml ``` The OpenAPI document includes the retry configuration for the `listPublications` operation: ```yaml filename="openapi.yaml" x-speakeasy-retries: statusCodes: - 5XX backoff: initialInterval: 500 maxInterval: 60000 maxElapsedTime: 3600000 exponent: 1.5 strategy: backoff retryConnectionErrors: true ``` After recreating the SDK using Speakeasy: ```bash speakeasy quickstart ``` The created SDK now includes retry logic for the `listPublications` operation, automatically handling network errors and `5XX` responses. ### Issues and feedback Need some assistance or have a suggestion? Reach out to our team at [support@speakeasy.com](mailto:support@speakeasy.com). If you haven't already, take a look at our [blog](/blog) to learn more about OpenAPI, SDK generation, and more, including: - [Native JSONL support in your SDKs](/blog/release-jsonl-support) - [Comprehensive SDK testing](/blog/release-sdk-testing) - [Model Context Protocol: TypeScript SDKs for the agentic AI ecosystem](/blog/release-model-context-protocol) # How to generate an OpenAPI/Swagger spec with tRPC Source: https://speakeasy.com/openapi/frameworks/trpc import { Callout } from "@/mdx/components"; In this tutorial, we'll explore how to generate an OpenAPI document for our [tRPC](https://trpc.io/) API, and then we'll use this document to create an SDK using Speakeasy. Here's what we'll cover: 1. Adding `trpc-openapi` to a tRPC project. 2. Generating an OpenAPI specification using `trpc-openapi`. 3. Improving the generated OpenAPI specification for better downstream SDK generation. 4. Using the Speakeasy CLI to create an SDK based on the generated OpenAPI specification. 5. Using the Speakeasy OpenAPI extensions to improve created SDKs. 6. Automating this process as part of a CI/CD pipeline. <Callout title="Tip"> If you want to follow along, you can use the [**tRPC Speakeasy Bar example repository**](https://github.com/speakeasy-api/speakeasy-trpc-example). </Callout> ## The SDK Creation Pipeline tRPC does not natively export OpenAPI documents, but the [`trpc-openapi`](https://github.com/jlalmes/trpc-openapi/) package adds this functionality. We'll start this tutorial by adding `trpc-openapi` to a project, and then we'll add a script to generate an OpenAPI schema and save it as a file. The quality of your OpenAPI specification will ultimately determine the quality of created SDKs and documentation, so we'll dive into ways you can improve the generated specification. With our new and improved OpenAPI specification in hand, we'll take a look at how to create SDKs using Speakeasy. Finally, we'll add this process to a CI/CD pipeline so that Speakeasy automatically creates fresh SDKs whenever your tRPC API changes in the future. ## Requirements To follow along with this tutorial, you will need: - An existing tRPC app, or you can clone our example application. - Some familiarity with tRPC. - [Node.js](https://nodejs.org/en/download) installed (we used Node v20.5.1). - The [Speakeasy CLI](/docs/speakeasy-cli/) installed. You'll use the CLI to create the SDK once you have generated your OpenAPI spec. ## Supported OpenAPI Versions Speakeasy supports OpenAPI v3 and v3.1. As of October 2023, `trpc-openapi` can generate schemas that adhere to the [OpenAPI v3.0.3 specification](https://spec.openapis.org/oas/v3.0.3). This OpenAPI version is not a limitation, but it is important to keep the versions used in mind when debugging code generation. Refer to the OpenAPI Initiative for an overview of the [differences between OpenAPI 3.0 and 3.1.0](https://www.openapis.org/blog/2021/02/16/migrating-from-openapi-3-0-to-3-1-0). ## Generate an OpenAPI/Swagger spec with tRPC We'll use [`trpc-openapi`](https://github.com/jlalmes/trpc-openapi/) to create REST endpoints for our tRPC procedures and then create the OpenAPI spec that describes these endpoints. To generate an OpenAPI spec for tRPC, we'll use [trpc-openapi](https://github.com/jlalmes/trpc-openapi/) to create REST endpoints for our tRPC procedures, then create an OpenAPI document that describes these endpoints. ### Add trpc-openapi to a Project Install `trpc-openapi`: ```bash npm install trpc-openapi ``` Use `initTRPC.meta<OpenApiMeta>()` to create a new tRPC instance with OpenAPI support: ```typescript filename="router.ts" import { initTRPC } from "@trpc/server"; import { OpenApiMeta } from "trpc-openapi"; const t = initTRPC.meta<OpenApiMeta>().create(); ``` Add OpenAPI meta to a procedure by passing an `openapi` object to the `meta` function. This object contains the HTTP method and path for the generated REST endpoint. ```typescript filename="router.ts" import { initTRPC } from "@trpc/server"; import { OpenApiMeta } from "trpc-openapi"; import { z } from "zod"; const t = initTRPC.meta<OpenApiMeta>().create(); export const appRouter = t.router({ findByProductCode: t.procedure .meta({ openapi: { method: "GET", path: "/find" } }) .input(z.object({ code: z.string() })) .output(z.object({ drink: z.object({ name: z.string() }) })) .query(async ({ input }) => { const drink = { name: "Old Fashioned", }; return { drink: drink }; }), }); ``` Add a new script to generate an OpenAPI document based on the tRPC router: ```typescript filename="openapi.ts" import { generateOpenApiDocument } from "trpc-openapi"; import { appRouter } from "./router"; export const openApiDocument = generateOpenApiDocument(appRouter, { title: "tRPC OpenAPI", version: "1.0.0", baseUrl: "http://localhost:3000", }); ``` Add a script to save the generated OpenAPI document to a file: ```typescript filename="generateOpenApi.ts" import { openApiDocument } from "./openapi"; console.log(JSON.stringify(openApiDocument, null, 2)); ``` Run the script to generate an OpenAPI document: ```bash ts-node generateOpenApi.ts > openapi-spec.json ``` Add this document to the `package.json` file as a script: ```json filename="package.json" { "scripts": { "generate-openapi": "ts-node generateOpenApi.ts > openapi-spec.json" } } ``` From now on, we can generate an OpenAPI document by running `npm run generate-openapi`. When we inspect the generated OpenAPI document, we can see that it contains a single endpoint for the `findByProductCode` procedure, but it's missing a lot of information. Let's see how we can improve this document. ## How To Improve the OpenAPI Info Section The OpenAPI info section contains information about the API, such as the title, description, and version. If you use Speakeasy later, it will use this information to create documentation and SDKs. The `GenerateOpenApiDocumentOptions` type from `trpc-openapi` allows us to add this information to our OpenAPI document: ```typescript filename="node_modules/trpc-openapi/dist/generator/index.d.ts" export type GenerateOpenApiDocumentOptions = { title: string; description?: string; version: string; baseUrl: string; docsUrl?: string; tags?: string[]; securitySchemes?: OpenAPIV3.ComponentsObject["securitySchemes"]; }; ``` We can add this information to our `generateOpenApiDocument` call: ```typescript filename="openapi.ts" import { generateOpenApiDocument } from "trpc-openapi"; import { appRouter } from "./router"; export const openApiDocument = generateOpenApiDocument(appRouter, { title: "Speakeasy Bar API", description: "An API to order drinks from the Speakeasy Bar", version: "1.0.0", baseUrl: "http://localhost:3000", docsUrl: "http://example.com/docs", tags: ["drinks"], }); ``` Run `npm run generate-openapi` and see how this information is added to the OpenAPI document: ```json filename="openapi-spec.json" { "openapi": "3.0.3", "info": { "title": "Speakeasy Bar API", "description": "An API to order drinks from the Speakeasy Bar", "version": "1.0.0" }, "servers": [ { "url": "http://localhost:3000" } ], "tags": [ { "name": "drinks" } ], "externalDocs": { "url": "http://example.com/docs" } // ... } ``` ## Improving the OpenAPI Paths We can improve our OpenAPI document by adding fields to the procedure's input and output schemas, and by adding examples, documentation, and metadata. ### Expanding the Procedure's Input and Output Schemas Let's create a `Drink` model and add a few field types to see how these are represented in the OpenAPI document. Create a new file called `models.ts` and specify a `Drink` model using Zod: ```typescript filename="models.ts" import { z } from "zod"; const DrinkType = z .enum(["NON_ALCOHOLIC", "BEER", "WINE", "SPIRIT", "OTHER"]) .describe("The type of drink"); type DrinkType = z.infer<typeof DrinkType>; export const ProductCode = z.string().describe("The product code of the drink"); export type ProductCode = z.infer<typeof ProductCode>; export const DrinkSchema = z.object({ name: z.string().describe("The name of the drink"), type: DrinkType, price: z.number().describe("The price of the drink"), stock: z.number().describe("The number of drinks in stock"), productCode: ProductCode, description: z.string().nullable().describe("A description of the drink"), }); export type Drink = z.infer<typeof DrinkSchema>; ``` Back in the `router.ts` file, import these models and update the procedure's input and output schemas. In the example app, we also added a mock database. ```typescript filename="router.ts" import { initTRPC } from "@trpc/server"; import { OpenApiMeta } from "trpc-openapi"; import { z } from "zod"; import { db } from "./db"; // Mock database import { DrinkSchema, ProductCode } from "./models"; const t = initTRPC.meta<OpenApiMeta>().create(); export const appRouter = t.router({ findByProductCode: t.procedure .meta({ openapi: { method: "GET", path: "/find" } }) .input(z.object({ code: ProductCode })) .output(z.object({ drink: DrinkSchema.optional() })) .query(async ({ input }) => { const drink = await db.drink.findByProductCode(input.code); return { drink: drink }; }), }); ``` If we regenerate the OpenAPI document, we can see that the `Drink` model is now included in the document with all of its fields: ```json filename="openapi-spec.json" { "paths": { "/find": { "get": { "operationId": "findByProductCode", "summary": "Find a drink by product code", "description": "Pass the product code of the drink to search for", "tags": ["drinks"], "parameters": [ { "name": "code", "in": "query", "required": true, "schema": { "type": "string" }, "description": "The product code of the drink", "example": "1234" } ], "responses": { "200": { "description": "Successful response", "content": { "application/json": { "schema": { "type": "object", "properties": { "drink": { "type": "object", "properties": { "name": { "type": "string", "description": "The name of the drink" }, "type": { "type": "string", "enum": [ "NON_ALCOHOLIC", "BEER", "WINE", "SPIRIT", "OTHER" ], "description": "The type of drink" }, "price": { "type": "number", "description": "The price of the drink" }, "stock": { "type": "number", "description": "The number of drinks in stock" }, "productCode": { "type": "string", "description": "The product code of the drink" }, "description": { "type": "string", "nullable": true, "description": "A description of the drink" } }, "required": [ "name", "type", "price", "stock", "productCode", "description" ], "additionalProperties": false } }, "additionalProperties": false }, "example": { "drink": { "name": "Beer", "type": "BEER", "price": 5, "stock": 10, "productCode": "1234", "description": "A nice cold beer" } } } } }, "default": { "$ref": "#/components/responses/error" } } } } } // ... } ``` Speakeasy will use the descriptions in these fields to create documentation for the SDK. ### OpenAPI Model Schemas in tRPC At Speakeasy, we recommend using OpenAPI model schemas so that schemas are reusable across multiple procedures. This simplifies SDK code creation, makes it easier to maintain your OpenAPI document, and provides a better developer experience for your users. At the time of writing, there is an [open issue on the tRPC repository](https://github.com/trpc/trpc-openapi/issues/157) to add support for OpenAPI model schemas. Until this is implemented, we'll need to be content with the duplication of schemas across procedures. Under the hood, `trpc-openapi` uses the [Zod to Json Schema](https://www.npmjs.com/package/zod-to-json-schema) package, which supports custom strategies for generating references to schemas, but this functionality is not yet exposed in `trpc-openapi`. ### Adding a Summary, Description, Examples, and Tags to a Procedure We can enrich our OpenAPI document by adding a summary, description, examples, and tags to our procedure's metaobject. The `trpc-openapi` package uses these fields to generate the `summary`, `description`, `example`, and `tags` fields in the OpenAPI document. The `example` field is also used to add examples to the input schema. ```typescript filename="router.ts" import { initTRPC } from "@trpc/server"; import { OpenApiMeta } from "trpc-openapi"; import { z } from "zod"; import { db } from "./db"; import { DrinkSchema, ProductCode } from "./models"; const t = initTRPC.meta<OpenApiMeta>().create(); export const appRouter = t.router({ findByProductCode: t.procedure .meta({ openapi: { method: "GET", path: "/find", summary: "Find a drink by product code", description: "Pass the product code of the drink to search for", tags: ["drinks"], example: { request: { code: "1234", }, response: { drink: { name: "Beer", type: "BEER", price: 5.0, stock: 10, productCode: "1234", description: "A nice cold beer", }, }, }, }, }) .input(z.object({ code: ProductCode })) .output(z.object({ drink: DrinkSchema.optional() })) .query(async ({ input }) => { const drink = await db.drink.findByProductCode(input.code); return { drink: drink }; }), }); ``` If we regenerate the OpenAPI document now, we can see that the `summary`, `description`, and `example` fields have been added to it. ### Add Metadata to Tags The `trpc-openapi` package defines tags as a list of strings, but OpenAPI allows you to add metadata to tags. For example, you can add a description or a link to documentation to a tag. Since `trpc-openapi` uses the [`openapi-types` package](https://www.npmjs.com/package/openapi-types) `OpenAPIV3.Document` type, which allows tags defined as a list of strings or a list of objects, we can extend our document to include tag objects with metadata even though `trpc-openapi` uses a list of strings. Let's add a description to the `drinks` tag: ```typescript filename="openapi.ts" import { generateOpenApiDocument } from "trpc-openapi"; import { appRouter } from "./router"; const openApiDocument = generateOpenApiDocument(appRouter, { title: "Speakeasy Bar API", description: "An API to order drinks from the Speakeasy Bar", version: "1.0.0", baseUrl: "http://localhost:3000", docsUrl: "http://example.com/docs", tags: ["drinks"], }); // add metadata to tags openApiDocument.tags = [ { name: "drinks", description: "Operations related to drinks", }, ]; export { openApiDocument }; ``` Now we can see that the `description` field has been added to the `drinks` tag in the generated OpenAPI document: ```json filename="openapi-spec.json" { "tags": [ { "name": "drinks", "description": "Operations related to drinks" } ] } ``` ### Add Speakeasy Extensions to Methods The OpenAPI vocabulary can sometimes be insufficient for your code creation needs. For these situations, Speakeasy provides a set of OpenAPI extensions. For example, you may want to give an SDK method a different name from the `OperationId`. You can use the Speakeasy `x-speakeasy-name-override` extension to do so. This time, unfortunately, the [`openapi-types` package](https://www.npmjs.com/package/openapi-types) `OperationObject` type does not allow for custom extensions, so we need to add the extension to the generated OpenAPI document manually. Ideally, we would create a new type that extends `OpenAPIV3.Document` and adds the `x-speakeasy-name-override` extension to the `OperationObject` type, but for this tutorial, we'll keep it simple and add the extension by casting the path item to `any`. Let's add an `x-speakeasy-name-override` extension to the `findByProductCode` procedure. First, we extend the `OpenAPIV3.OperationObject` and `OpenAPIV3.Document` types to add the `x-speakeasy-name-override` and other extensions: ```typescript filename="extended-types.ts" import { OpenAPIV3 } from "openapi-types"; export type IExtensionName = `x-${string}`; export type IExtensionType = any; export type ISpecificationExtension = { [extensionName: IExtensionName]: IExtensionType; }; export type ExtendedDocument = OpenAPIV3.Document & ISpecificationExtension; export type ExtendedOperationObject = OpenAPIV3.OperationObject<ISpecificationExtension>; ``` Then we import our extended operation type and add the `x-speakeasy-name-override` extension to the `findByProductCode` procedure: ```typescript filename="openapi.ts" import { generateOpenApiDocument } from "trpc-openapi"; import { ExtendedOperationObject } from "./extended-types"; import { appRouter } from "./router"; const openApiDocument = generateOpenApiDocument(appRouter, { title: "Speakeasy Bar API", description: "An API to order drinks from the Speakeasy Bar", version: "1.0.0", baseUrl: "http://localhost:3000", docsUrl: "http://example.com/docs", tags: ["drinks"], }); // add metadata to tags openApiDocument.tags = [ { name: "drinks", description: "Operations related to drinks", }, ]; if ( openApiDocument.paths && openApiDocument.paths["/find"] && openApiDocument.paths["/find"].get ) { (openApiDocument.paths["/find"].get as ExtendedOperationObject)[ "x-speakeasy-name-override" ] = "searchDrink"; } export { openApiDocument }; ``` ## Add Retries to an SDK With `x-speakeasy-retries` Speakeasy can create SDKs that follow custom rules for retrying failed requests. For instance, if your server fails to return a response within a specified time, you may want your users to retry their request without clobbering your server. Add retries to Speakeasy-created SDKs by adding a top-level `x-speakeasy-retries` schema to your OpenAPI spec. You can also override the retry strategy per operation by adding `x-speakeasy-retries`. ### Adding Global Retries and Retries per Endpoint Let's add a global retry strategy to our OpenAPI document and override it for our `findByProductCode` procedure. ```typescript filename="openapi.ts" import { generateOpenApiDocument } from "trpc-openapi"; import { ExtendedDocument, ExtendedOperationObject } from "./extended-types"; import { appRouter } from "./router"; const openApiDocument = generateOpenApiDocument(appRouter, { title: "Speakeasy Bar API", description: "An API to order drinks from the Speakeasy Bar", version: "1.0.0", baseUrl: "http://localhost:3000", docsUrl: "http://example.com/docs", tags: ["drinks"], }); // add metadata to tags openApiDocument.tags = [ { name: "drinks", description: "Operations related to drinks", }, ]; (openApiDocument as ExtendedDocument)["x-speakeasy-retries"] = { strategy: "backoff", backoff: { initialInterval: 500, maxInterval: 60000, maxElapsedTime: 3600000, exponent: 1.5, }, statusCodes: ["5XX"], retryConnectionErrors: true, }; if ( openApiDocument.paths && openApiDocument.paths["/find"] && openApiDocument.paths["/find"].get ) { (openApiDocument.paths["/find"].get as ExtendedOperationObject)[ "x-speakeasy-name-override" ] = "searchDrink"; (openApiDocument.paths["/find"].get as ExtendedOperationObject)[ "x-speakeasy-retries" ] = { strategy: "backoff", backoff: { initialInterval: 500, maxInterval: 60000, maxElapsedTime: 3600000, exponent: 1.5, }, statusCodes: ["5XX"], retryConnectionErrors: true, }; } export { openApiDocument }; ``` Regenerate the OpenAPI document and you can see that the `x-speakeasy-retries` field has been added to the document. ```json filename="openapi-spec.json" { "x-speakeasy-retries": { "strategy": "backoff", "backoff": { "initialInterval": 500, "maxInterval": 60000, "maxElapsedTime": 3600000, "exponent": 1.5 }, "statusCodes": ["5XX"], "retryConnectionErrors": true } // ... } ``` ## Why Speakeasy and tRPC? tRPC's focus on type safety and developer experience sets it apart from other TypeScript API frameworks. By using TypeScript's type system along with a schema library like Zod, tRPC allows your server and client code to share types. One of tRPC's stated goals is to cut down on the need for codegen, but we believe there is still a place for code generation in the tRPC ecosystem. While tRPC's [default client](https://trpc.io/docs/client/vanilla/setup) is useful for writing internal clients in a monorepo where a client can import the server's `AppRouter`, it does not make it easy to publish production-ready SDKs for use by internal and external developers. Nor does tRPC's type-safety extend to SDKs in languages other than TypeScript. Speakeasy can help you create type-safe, production-ready SDKs for your tRPC API in various languages so that you can focus on building your API, confident that your users will have a great developer experience. ## How To Create an SDK Based on the OpenAPI Spec After following the steps above, we have an OpenAPI spec that is ready to use as the basis for a new SDK. Now we'll use Speakeasy to create an SDK. In the root directory of your project, run the following: ```bash speakeasy quickstart ``` Follow the onscreen prompts to provide the necessary configuration details for your new SDK such as the name, schema location and output path. Enter `openapi-spec.json` when prompted for the OpenAPI document location and select TypeScript when prompted for which language you would like to generate. ## Add SDK Creation to GitHub Actions The Speakeasy [`sdk-generation-action`](https://github.com/speakeasy-api/sdk-generation-action) repository provides workflows to integrate the Speakeasy CLI in your CI/CD pipeline so that your client SDKs are recreated when your OpenAPI spec changes. You can set up Speakeasy to automatically push a new branch to your SDK repositories so that your engineers can review and merge the SDK changes. For an overview of how to set up automation for your SDKs, see the Speakeasy [SDK Generation Action and Workflows](/docs/workflow-reference) documentation. # Automatically output OpenAPI from TypeScript with tsoa Source: https://speakeasy.com/openapi/frameworks/tsoa import { Callout } from "@/mdx/components"; Anyone who has worked with OpenAPI specifications knows how useful they can be for documenting and sharing APIs. However, it can be a daunting task to create and maintain OpenAPI documents manually, especially as APIs evolve over time. Writing loads of OpenAPI by hand can be tedious and error-prone, but for years the only alternative was littering codebases with annotations. Fortunately, with [tsoa (TypeScript OpenAPI)](https://tsoa-community.github.io/docs/introduction.html), developers can write clean TypeScript code and generate OpenAPI specifications automatically. ## How tsoa works tsoa is a particularly clever tool. Powered by TypeScript's brilliant type system, it integrates with popular web application frameworks like express, Koa, and Hapi, to generate routes and middlewares that now only generate OpenAPI documents. This means the application and the documentation are running from a single source of truth for the API, powering runtime validation, contract testing, SDK generation, and anything else that has an [OpenAPI tool](https://openapi.tools/) to do. The types tsoa uses are standard TypeScript interfaces and types, meaning they can be used throughout an application the same as any other type. ```ts export interface User { id: number; email: string; name: string; status?: "Happy" | "Sad"; phoneNumbers: string[]; } ``` ## Creating an OpenAPI document with tsoa It's easiest to imagine this being done on a brand new application, but it could be added to an existing codebase as well. Equally this is simpler to conceptualize when no OpenAPI exists already, but tooling exists to generate TypeScript types from OpenAPI as well. To keep things simple, this guide focuses on a new application and generates new OpenAPI. ### Step 1: Set Up a New TypeScript Project First, create a new directory for the project and initialize a new Node.js project with TypeScript support. ```bash mkdir speakeasy-tsoa-example cd speakeasy-tsoa-example yarn init -y yarn add tsoa express yarn add -D typescript @types/node @types/express yarn run tsc --init ``` There are two config files to set up here. First, configure `tsconfig.json` for tsoa: ```json filename="tsconfig.json" { "compilerOptions": { "incremental": true, "target": "es2022", "module": "node18", "outDir": "build", "strict": true, "noUnusedLocals": true, "noUnusedParameters": true, "noImplicitReturns": true, "noFallthroughCasesInSwitch": true, "moduleResolution": "node16", "baseUrl": ".", "esModuleInterop": true, "resolveJsonModule": true, "experimentalDecorators": true, "forceConsistentCasingInFileNames": true, // Avoid type checking 3rd-party libs (e.g., optional Hapi/Joi types) "skipLibCheck": true }, "exclude": [ "./sdk", ] } ``` There's a fair few options there but it'll help get tsoa running as expected. Now to configure tsoa itself, create a `tsoa.json` file in the root of the project: ```json filename="tsoa.json" { "entryFile": "src/app.ts", "noImplicitAdditionalProperties": "throw-on-extras", "controllerPathGlobs": ["src/app/*Controller.ts"], "spec": { "outputDirectory": "build", "specFileBaseName": "openapi", "specVersion": 3.1 }, "routes": { "routesDir": "build" } } ``` <Callout title="Note" type="info"> In December 2025 tsoa added support for OpenAPI v3.1, in the version v7.0.0-alpha0. Legacy versions v3.0 and v2.0 are also supported, but with v3.2 already being out it's good to work with the latest version all tools support. Speakeasy already supports v3.2, but will work with v3.1 documents just fine. Make sure to set the `specVersion` in the `tsoa.json` file to `3.1` as shown above, or use `2` or `3` for older versions of OpenAPI. ```json filename="tsoa.json" { "spec": { "specVersion": 3.1 } } ``` </Callout> ### Step 2: Define Models This section defines a simple Booking model that can be used in the example application. Create a new file `src/app/models/booking.ts` and add something like this to describe all the properties that can be in this payload. ```ts filename="src/app/models/booking.ts" export interface Booking { id: string; trip_id: string; passenger_name: string; has_bicycle?: boolean; has_dog?: boolean; } ``` This will create very rudimentary OpenAPI output but it can be enhanced further with comments and decorators. More on that later. ### Step 3: Create a Service Layer It's a good idea to create a Service that handles interaction with the application's models instead of shoving all that logic into the controller layer. ```ts filename="src/app/bookingService.ts" import { bookings } from "./fixtures"; import { Booking } from "./models/booking"; export class BookingsService { public list(page = 1, limit = 10): Booking[] { const start = (page - 1) * limit; return bookings.slice(start, start + limit); } public get(bookingId: string): Booking | undefined { return bookings.find((b) => b.id === bookingId); } public create(input: Omit<Booking, "id">): Booking { const id = crypto.randomUUID(); const booking: Booking = { id, ...input }; bookings.push(booking); return booking; } } ``` ### Step 4: Create a Controller With the model and service layer set up, the next step is to create a controller to handle incoming HTTP requests. Create a new file `src/app/bookingController.ts` and add the following code: ```ts filename="src/app/bookingsController.ts" import { Body, Controller, Delete, Get, Path, Post, Query, Res, Route, Tags, TsoaResponse } from "tsoa"; import { Booking } from "./models/booking"; import { BookingsService } from "./bookingsService"; @Route("bookings") @Tags("Bookings") export class BookingsController extends Controller { @Get() public async listBookings( @Query() page?: number, @Query() limit?: number ): Promise<Booking[]> { return new BookingsService().list(page ?? 1, limit ?? 10); } @Get("{bookingId}") public async getBooking( @Path() bookingId: string, @Res() notFound: TsoaResponse<404, { reason: string }> ): Promise<Booking> { const booking = new BookingsService().get(bookingId); if (!booking) return notFound(404, { reason: "Booking not found" }); return booking; } @SuccessResponse("201", "Created") // Custom success response @Post() public async createBooking( @Body() requestBody: Omit<Booking, "id"> ): Promise<Booking> { return new BookingsService().create(requestBody); } } ``` This is the first sign of tsoa-specific code being brought into the application. Unlike older OpenAPI/Swagger tools like swagger-jsdoc, tsoa uses actual decorators that modify the behavior of the code at runtime, a huge improvement on the old code comments approach because they were just floating near the production code and could potentially disagree. The `@Route()` decorator sets out the first chunk of the URI, so if the API is running on `https://example.com/api/booking` that is a server path of `https://example.com/api/` and a `@Route('bookings')` to create the whole thing. Additionally, we define 2 methods: `listBookings` to list all bookings with optional pagination, and `getBooking` to retrieve a specific booking by its ID. The `createBooking` method allows us to create a new booking by sending a `POST` request with the booking details in the request body. Each time these methods are decorated with HTTP method decorators like `@Get()` and `@Post()`, which map them to the corresponding HTTP methods, and providing extra URL path information where needed. The `@Get("{bookingId}")` decorator indicates that this method will handle `GET` requests to the `/bookings/{bookingId}` endpoint, where `{bookingId}` is a path parameter that will be replaced with the actual booking ID when making the request. This syntax is closely mirroring OpenAPI's path templating for compatibility reasons. Path templating refers to the usage of template expressions, delimited by curly braces ({}), to mark a section of a URL path as replaceable using path parameters. Understanding [parameters in OpenAPI](/openapi/requests/parameters) will help learning how tsoa parameters, but put simply tsoa will allow 4 types of parameters: Path parameters (using `@Path()`), Query Parameters (`@Query()` or `@Queries()`), Header Parameters (`@Header()`) and Body Parameters (`@Body()` or individual properties using `@BodyProp()`). ### Step 5: Set up the Express app Let's now create an `app.ts` and a `server.ts` file in our source directory like this: ```ts filename="src/app.ts" import express, {json, urlencoded} from "express"; import { RegisterRoutes } from "../build/routes"; export const app = express(); // Use body parser to read sent json payloads app.use( urlencoded({ extended: true, }) ); app.use(json()); RegisterRoutes(app); ``` Another file `server.ts` can be created to set the application to listen on a port: ```ts filename="src/server.ts" import { app } from "./app"; const port = process.env.PORT || 3000; app.listen(port, () => console.log(`Example app listening at http://localhost:${port}`) ); ``` This is a pretty standard express setup, but that `RegisterRoutes` import might look a little funny to anyone used to working with express alone. ### Step 6: Building the routes file At this point you may have noticed that TypeScript will not find the `RegisterRoutes` import from build/routes. That's because we haven't asked tsoa to create that yet. Let's do that now: ```shell mkdir -p build yarn run tsoa routes ``` The `tsoa routes` command generates the routes file based on the controllers defined in the project. It reads the configuration from the `tsoa.json` file and creates the necessary route definitions in the specified output directory, putting the generated file into `build/routes.ts`. This routes file is autogenerated and it is not necessary to know too much about how it works, but in short it maps the HTTP routes to the controller methods defined earlier, handling request validation and response formatting based on the decorators and types used in the controllers. ```ts filename="build/routes.ts" /* tslint:disable */ /* eslint-disable */ import type { TsoaRoute } from '@tsoa/runtime'; import { fetchMiddlewares, ExpressTemplateService } from '@tsoa/runtime'; import { TripsController } from './../src/app/tripsController'; import { StationsController } from './../src/app/stationsController'; import { BookingsController } from './../src/app/bookingsController'; import type { Request as ExRequest, Response as ExResponse, RequestHandler, Router } from 'express'; const models: TsoaRoute.Models = { "Booking": { "dataType": "refObject", "properties": { "id": {"dataType":"string","required":true}, "trip_id": {"dataType":"string","required":true}, "passenger_name": {"dataType":"string","required":true}, "has_bicycle": {"dataType":"boolean","default":false}, "has_dog": {"dataType":"boolean","default":false}, }, "additionalProperties": false, }, // ... snipped for brevity }; // Then it autogenerates route handlers like: app.get('/bookings', ...(fetchMiddlewares<RequestHandler>(BookingsController)), ...(fetchMiddlewares<RequestHandler>(BookingsController.prototype.getBooking)), async function BookingsController_getBooking(request: ExRequest, response: ExResponse, next: any) { ``` Automatically generating the routes part may feel odd at first, but it may also feel like an incredibly welcome change as the tedious boilerplate has been handled automatically, and can be regenerated over and over as the application evolves. Either way, with the `build/routes.ts` file now created, it's time to compile TypeScript and start the server: ```shell yarn run tsc node build/src/server.js ``` <Callout title="Tip" > It can be helpful to add these scripts to `package.json` at this point. This enables the use of `yarn build` and `yarn start` commands: ```js filename="package.json" "main": "build/src/server.js", "scripts": { "build": "tsoa spec-and-routes && tsc", "start": "node build/src/server.js" }, ``` </Callout> ### Step 6: Generate the OpenAPI Spec The final part is to generate OpenAPI from this application. The easiest way to do this is using the [tsoa CLI](https://tsoa-community.github.io/docs/generating.html#using-cli) by running the following command in the terminal: ```bash yarn run tsoa spec # or yarn run tsoa spec --yaml ``` Doing this will create a `build/openapi.json` or `build/openapi.yaml` document containing the OpenAPI description for the API. The YAML version is easier to read, as shown below. ```yaml filename="build/openapi.yaml" openapi: 3.1.0 info: title: speakeasy-tsoa-example version: 2.0.0 description: Speakeasy Train Travel tsoa API license: name: Apache-2.0 contact: name: "Speakeasy Support" email: support@speakeasy.com components: schemas: Booking: properties: id: type: string description: Unique identifier for the booking. example: 3f3e3e1-c824-4d63-b37a-d8d698862f1d format: uuid trip_id: type: string description: Identifier of the booked trip. example: 4f4e4e1-c824-4d63-b37a-d8d698862f1d format: uuid passenger_name: type: string description: Name of the passenger. example: John Doe has_bicycle: type: boolean description: Indicates whether the passenger has a bicycle. example: true default: false has_dog: type: boolean description: Indicates whether the passenger has a dog. example: false default: false required: - id - trip_id - passenger_name type: object additionalProperties: false paths: /bookings: get: operationId: ListBookings responses: "200": description: Ok content: application/json: schema: items: $ref: "#/components/schemas/Booking" type: array tags: - Bookings security: [] parameters: - in: query name: page required: false schema: format: double type: number - in: query name: limit required: false schema: format: double type: number post: operationId: CreateBooking responses: "200": description: Ok content: application/json: schema: $ref: "#/components/schemas/Booking" tags: - Bookings security: [] parameters: [] requestBody: required: true content: application/json: schema: $ref: "#/components/schemas/Omit_Booking.id_" ``` Not a bad start, but this can be improved by learning more about the decorators and options available in tsoa. ### Step 7: Improving the OpenAPI Output Improvements may well be an ongoing process, but the first and most important step is to make sure the TypeScript types representing "models" in the application are well defined. Adding comments to the properties of interfaces and types will help tsoa generate better descriptions in the OpenAPI document. Using a combination of JSDoc comments and tsoa decorators, developers can provide additional metadata for each property, such as examples, formats, and constraints. ```typescript filename="src/app/models/booking.ts" export interface Booking { /** * Unique identifier for the booking. * @format uuid * @example "3f3e3e1-c824-4d63-b37a-d8d698862f1d" * @readonly */ id: string; /** * Identifier of the booked trip. * @format uuid * @example "4f4e4e1-c824-4d63-b37a-d8d698862f1d" */ trip_id: string; /** * Name of the passenger. * @example "John Doe" */ passenger_name: string; /** * Indicates whether the passenger has a bicycle. * @default false * @example true */ has_bicycle?: boolean; /** * Indicates whether the passenger has a dog. * @default false * @example false */ has_dog?: boolean; } ``` Adding this extra context is not just beneficial for generating a more informative OpenAPI document, but it will power real runtime functionality too. Types will be checked and validated at runtime, preventing invalid requests getting anywhere near the application logic. ![](/assets/openapi/tsoa/tsoa-type-checking.png) All sorts of things can happen with this extra metadata: - Types will be checked and validated at runtime. - Additional properties in JSON will trigger validation errors. - Default values will actually be used when creating resources. - Any `@readonly` properties will be ignored when sent from request bodies. Anyone up for a challenge can even add regex patterns to string properties using the `@pattern` decorator. ```ts /** * Identifier of the booked trip. * @format uuid * @example "4f4e4e1-c824-4d63-b37a-d8d698862f1d" * @pattern ^[0-9a-fA-F]{8}\b-[0-9a-fA-F]{4}\b-[0-9a-fA-F]{4}\b-[0-9a-fA-F]{4}\b-[0-9a-fA-F]{12}$ */ trip_id: string; ``` ![](/assets/openapi/tsoa/tsoa-regex-validation.png) Tweak models like this and regenerate the OpenAPI to see the improvements reflected in the output. ### (Optional) Step 8: Add a /docs endpoint to Serve OpenAPI Documentation To make it easier to explore and share API documentation, add a `/docs` endpoint to the Express application that serves the OpenAPI document. ```ts filename="src/app.ts" focus=5:17 export const app = express(); // ... other middleware and route registrations // Serve the OpenAPI spec app.use("/openapi.json", (req: ExRequest, res: ExResponse) => { res.sendFile("openapi.json", { root: __dirname + "/../build" }); }); // Serve API reference documentation using dynamic import (ESM-only package) (async () => { const { apiReference } = await import("@scalar/express-api-reference"); app.use("/docs", apiReference({ url: "/openapi.json" })); })(); ``` ## Improving OpenAPI output further There are many more ways to improve the OpenAPI output generated by tsoa: ### Set OpenAPI info section By default tsoa will take values from `package.json` to popular the `info` section of the OpenAPI document, but the team maintaining the codebase might not the best point of contact to help public/external API consumers. To manually configure the OpenAPI `info` section for contact and any other values, configure pop them in the "spec" portion of the `tsoa.json` file: ```json filename="tsoa.json" focus=8:15 { "entryFile": "src/app.ts", "noImplicitAdditionalProperties": "throw-on-extras", "controllerPathGlobs": ["src/app/*Controller.ts"], "spec": { "outputDirectory": "build", "specFileBaseName": "openapi", "specVersion": 3.1, "name": "Custom API Name", "description": "Custom API Description", "license": "MIT", "version": "1.1.0", "contact": { "name": "API Contact", "email": "help@example.com", "url": "http://example.com" } } } ``` ### Reusable component schemas This section shows how to help tsoa generate separate and reusable component schemas for a request body. Consider the following Trip model: ```typescript filename="src/app/models/trip.ts" export interface Trip { /** * Unique identifier for the trip. * @format uuid * @example "ea399ba1-6d95-433f-92d1-83f67b775594" */ id: string; /** * The origin station ID. * @format uuid * @example "efdbb9d1-02c2-4bc3-afb7-6788d8782b1e" */ origin: string; /** * The destination station ID. * @format uuid * @example "b2e783e1-c824-4d63-b37a-d8d698862f1d" */ destination: string; /** * Departure time in ISO 8601 format. * @format date-time * @example "2024-02-01T10:00:00Z" */ departure_time: string; /** * Arrival time in ISO 8601 format. * @format date-time * @example "2024-02-01T16:00:00Z" */ arrival_time: string; /** * The operator running the trip. * @example "Deutsche Bahn" */ operator: string; /** * The cost of the trip. * @example 50 */ price: number; /** * Indicates whether bicycles are allowed on the trip. * @default false * @example true */ bicycles_allowed?: boolean; /** * Indicates whether dogs are allowed on the trip. * @default false * @example true */ dogs_allowed?: boolean; } ``` The goal is to write a controller that updates the `operator` and `price` fields. The controller should take both fields as body parameters. The example controller below is a starting point. Note how the body parameters `operator` and `price` are defined by passing the `@BodyProp` decorator to the controller function multiple times. ```typescript filename="src/app/tripsController.ts" mark=6:7 @Route("trips") export class TripsController extends Controller { @Put("{tripId}") public async updateTrip( @Path() tripId: string, @BodyProp() operator?: string, @BodyProp() price?: number ): Promise<Trip> { const trip = new TripsService().updateTrip( tripId, operator, price ); return trip; } } ``` This would generate inline parameters without documentation for the `UpdateTrip` operation in OpenAPI, as shown in the snippet below: ```yaml filename="build/openapi.yaml" requestBody: required: true content: application/json: schema: properties: operator: type: string price: type: number type: object ``` While perfectly valid, this schema is not reusable and excludes the documentation and examples from our model definition. It is recommended to pick fields from the model interface directly and export a new interface. The TypeScript utility types `Pick` and `Partial` can be used to pick the `operator` and `price` fields and make both optional: ```typescript filename="src/app/tripsService.ts" export interface TripUpdateParams extends Partial<Pick<Trip, "operator" | "price">> {} ``` In the controller, `TripUpdateParams` can now be used as follows: ```typescript filename="src/app/tripsController.ts" mark=6 @Route("trips") export class TripsController extends Controller { @Put("{tripId}") public async updateTrip( @Path() tripId: string, @Body() requestBody: TripUpdateParams ): Promise<Trip> { const trip = new TripsService().updateTrip(tripId, requestBody); return trip; } } ``` ### Customizing OpenAPI operationId Using tsoa When generating an OpenAPI spec, tsoa adds an `operationId` to each operation. The `operationId` can be customized in three ways: - Using the `@OperationId` decorator. - Using the default tsoa `operationId` generator. - Creating a custom `operationId` template. #### Using the @OperationId decorator The most straightforward way to customize the `operationId` is to add the `@OperationId` decorator to each operation. In the example below, the custom `operationId` is `updateTripDetails`: ```typescript filename="src/app/tripsController.ts" mark=7 @Route("trips") export class TripsController extends Controller { @OperationId("updateTripDetails") @Put("{tripId}") public async updateTrip( @Path() tripId: string, @Body() requestBody: TripUpdateParams ): Promise<Trip> { const trip = new TripsService().updateTrip(tripId, requestBody); return trip; } } ``` #### Using the default operationId generator If a controller method is not decorated with the `OperationId` decorator, tsoa generates the `operationId` by converting the method name to title case using the following Handlebars template: ```handlebars {{titleCase method.name}} ``` #### Creating a custom operationId template To create a custom `operationId` for all operations without the `@OperationId` decorator, tsoa allows a Handlebars template to be specified in `tsoa.json`. tsoa adds two helpers to Handlebars: `replace` and `titleCase`. The method object and controller name get passed to the template as `method` and `controllerName`. The following custom `operationId` template prepends the controller name and removes underscores from the method name: ```json filename="tsoa.json" { "spec": { "operationIdTemplate": "{{controllerName}}-{{replace method.name '_' ''}}" } } ``` ### Add Speakeasy extensions to methods Sometimes OpenAPI's vocabulary is insufficient for certain generation needs. For these situations, Speakeasy provides a set of OpenAPI extensions. For example, an SDK method may need a name different from the `OperationId`. To cover this use case, Speakeasy provides an `x-speakeasy-name-override` extension. To add these custom extensions to an OpenAPI spec, it is possible to make use of tsoa's `@Extension()` decorator: ```typescript filename="src/app/tripsController.ts" mark=5 @Route("trips") export class TripsController extends Controller { @OperationId("updateTripDetails") @Extension({"x-speakeasy-name-override":"update"}) @Put("{tripId}") public async updateTrip( @Path() tripId: string, @Body() requestBody: TripUpdateParams ): Promise<Trip> { const trip = new TripsService().updateTrip(tripId, requestBody); return trip; } } ``` ## Add retries to Speakeasy SDKs Speakeasy can generate SDKs that follow custom rules for retrying failed requests. For instance, if a server fails to return a response within a specified time, it may be desirable for client applications to retry their request without clobbering the server. Add retries to SDKs generated by Speakeasy by adding a top-level `x-speakeasy-retries` schema to your OpenAPI spec. You can also override the retry strategy per operation by adding `x-speakeasy-retries`. ### Adding Global Retries To add a top-level retries extension to your OpenAPI spec, add a new `spec` schema to the `spec` configuration in `tsoa.json`: ```json filename="tsoa.json" { "spec": { "spec": { "x-speakeasy-retries": { "strategy": "backoff", "backoff": { "initialInterval": 500, "maxInterval": 60000, "maxElapsedTime": 3600000, "exponent": 1.5 }, "statusCodes": ["5XX"], "retryConnectionErrors": true } } } } ``` ### Adding retries per method To add retries to individual methods, use the tsoa `@Extension` decorator. In the example below, we add `x-speakeasy-retries` to the `updateTrip` method: ```typescript filename="src/app/tripsController.ts" mark=4:14 @Route("trips") export class TripsController extends Controller { @Put("{tripId}") @Extension("x-speakeasy-retries", { strategy: "backoff", backoff: { initialInterval: 500, maxInterval: 60000, maxElapsedTime: 3600000, exponent: 1.5, }, statusCodes: ["5XX"], retryConnectionErrors: true, }) public async updateTrip( @Path() tripId: string, @Body() requestBody: TripUpdateParams, ): Promise<Trip> { const trip = new TripsService().updateTrip(tripId, requestBody); return trip; } } ``` ## Generate an SDK based on the OpenAPI output Once an OpenAPI spec is available, use Speakeasy to generate an SDK by calling the following in the terminal: ```bash speakeasy quickstart ``` Follow the onscreen prompts to provide the necessary configuration details for the new SDK such as the name, schema location and output path. Enter `build/openapi.json` when prompted for the OpenAPI document location and select TypeScript when prompted for which language should be generated. SDKs can be generated using Speakeasy whenever the API definition in tsoa changes. Many Speakeasy users [add SDK generation to their CI workflows](/docs/workflow-reference) to ensure SDKs are always up to date. ## Summary This guide explored how to use tsoa to automatically generate OpenAPI specifications from TypeScript applications. It covered setting up a new TypeScript project, defining models, creating a service layer and controllers, and generating the OpenAPI document. It also discussed ways to improve the OpenAPI output using decorators and comments, as well as adding Speakeasy extensions for SDK generation. # How to Create OpenAPI Schemas and SDKs With TypeSpec Source: https://speakeasy.com/openapi/frameworks/typespec [TypeSpec](https://typespec.io/) is a brand-new domain-specific language (DSL) used to describe APIs. As the name implies you describe your API using a TypeScript-like type system, with language constructs such as `model` for the structure or schema of your API's data, or `op` for operations in your API. If you've used [OpenAPI](/openapi), these concepts likely sound familiar – this is because TypeSpec is also influenced by and generates OpenAPI. So something that is _like_ OpenAPI, and also generates OpenAPI specifications? You may be asking yourself, why does TypeSpec exist? Like many people, our initial reaction to TypeSpec was to reference the iconic XKCD strip: <div className="text-center"> <br /> <img src="https://imgs.xkcd.com/comics/standards_2x.png" className="w-[70%] mx-auto" alt="Standards" /> </div> However, after spending some time with it, we've come to understand the justification for a new DSL - we'll cover some of that shortly. We also ran into this young language's rough edges, and we'll cover those in detail, too. Our end goal with this article is to create a high-quality TypeScript SDK. However, before we create an SDK, we'll need to learn how to generate an OpenAPI document based on a TypeSpec specification. For that, we need to learn TypeSpec, and there is no better way to get started learning a new language than by asking _why_ it exists in the first place. ## The Problem TypeSpec Solves Code generation is a force multiplier in API design and development. When an executive unironically asks, "How do we 10x API creation?", the unironic answer is, " API-first design + Code generation." API-first means specifying exactly what your application's programming interface will look like before anything gets built, code generation means using that definition to create documentation, server (stubs) and client libraries (SDKs). As mentioned previously,OpenAPI is widely used for exactly this reason – it provides a human-readable (as YAML) specification format for APIs, and comes with a thriving ecosystem of tools and code generators. So if OpenAPI exists, what can TypeSpec add? The fundamental problem TypeSpec aims to solve is that writing OpenAPI documents by hand is complex, tedious, and error-prone. The complexity often leads to teams to abandon an API-first approach and instead start by coding their API, and then extracting OpenAPI from the codebase when they get to the point where they need documentation and SDKs – a quasi-API-first approach. Ultimately, OpenAPI isn't for everyone. Neither is TypeSpec for that matter. But for those who are immersed in the TypeScript ecosystem, TypeSpec may be a more natural fit than OpenAPI. And the more tools we have to help businesses create great APIs, the better. ## TypeSpec Development Status Before you trade in your OpenAPI YAML for TypeSpec, know that at the time of writing, TypeSpec is nowhere near as feature-rich and stable as OpenAPI. If you're designing a new API from scratch, taking the time to learn OpenAPI will benefit your team, even if TypeSpec one day becomes the most popular API specification language. ## TypeSpec Libraries and Emitters Developers can extend the capabilities of TypeSpec by creating and using libraries. These libraries can provide additional functionality, such as decorators, types, and operations, that are not part of the core TypeSpec language. A special type of library in TypeSpec is an emitter. Emitters are used to generate output from a TypeSpec specification. For example, the `@typespec/openapi3` library provides an emitter that generates an OpenAPI document from a TypeSpec specification. When targeting a specific output format, such as OpenAPI, you can use the corresponding emitter library to generate the desired output. This allows you to write your API specification in TypeSpec and then generate the output in the desired format. ## A Brief Introduction to TypeSpec Syntax This guide won't give a complete introduction or overview of TypeSpec, but we'll take a brief look at the language's structure and important concepts in the context of generating SDKs. ### Modularity in TypeSpec The main entry point in TypeSpec is the `main.tsp` file. This file has the same role as the `index.ts` file in a TypeScript project. Just like in TypeScript, we can organize code into files, folders, and modules, then [import](https://typespec.io/docs/language-basics/imports) these using the `import` statement. This helps split large API specifications into smaller, more manageable parts. The difference between TypeScript and TypeSpec in this regard is that TypeSpec imports files, not code. Here's an example of how you can import files, folders, and modules in TypeSpec: ```typescript filename="main.tsp" import "./books.tsp"; // Import a file import "./books"; // Import main.tsp in a folder import "/books"; // Import a TypeSpec module's main.tsp file ``` We can install modules using npm, and use the `import` statement to import them into our TypeSpec project. [Namespaces](https://typespec.io/docs/language-basics/namespaces), another TypeScript feature that TypeSpec borrows, allow you to group types and avoid naming conflicts. This is especially useful when importing multiple files that define types with the same name. Just like with TypeScript, namespaces may be nested and span multiple files. Namespaces are defined using the `namespace` keyword, followed by the namespace name and a block of type definitions. Here's an example: ```typescript namespace MyNamespace { model User { id: string; name: string; } } ``` They may also be defined at the file level, using the `namespace` keyword followed by the namespace name and a block of type definitions. Here's an example: ```typescript namespace MyNamespace; model User { id: string; name: string; } model Post { id: string; title: string; content: string; } ``` ### Models in TypeSpec [Models](https://typespec.io/docs/language-basics/models) in TypeSpec are similar to OpenAPI's `schema` objects. They define the structure of the data that will be sent and received by your API. We define models using the `model` keyword, followed by the model name and a block of properties. Here's an example: ```typescript filename="main.tsp" model User { id: string; name: string; email: string; } ``` Models are composable and extensible. You can reference other models within a model definition, extend a model with additional properties, and compose multiple models into a single model. Here's an example of model composition: ```typescript filename="main.tsp" namespace WithComposition { model User { id: string; name: string; email: string; } model HasRole { role: string; } model Admin is User { // Copies the properties and decorators from User ...HasRole; // Extends the User model with the properties from the HasRole model level: number; // Adds a new property to the Admin model } } // The Admin model above will have the following properties: namespace WithoutComposition { model Admin { id: string; name: string; email: string; role: string; level: number; } } ``` The equivalent OpenAPI specification for the `User` model above would look like this: ```yaml filename="openapi.yaml" components: schemas: User: type: object properties: id: type: string name: type: string email: type: string ``` ### Operations in TypeSpec [Operations](https://typespec.io/docs/language-basics/operations) in TypeSpec are similar to OpenAPI operations. They describe the methods that users can call in your API. We define operations using the `op` keyword, followed by the operation name. Here's an example: ```typescript filename="main.tsp" op listUsers(): User[]; // Defaults to GET op getUser(id: string): User; // Defaults to GET op createUser(@body user: User): User; // Defaults to POST with a body parameter ``` ### Interfaces in TypeSpec [Interfaces](https://typespec.io/docs/language-basics/interfaces) in TypeSpec group related operations together, similar to OpenAPI's `paths` object. We define interfaces using the `interface` keyword, followed by the interface name and a block of operations. Here's an example: ```typescript filename="main.tsp" @route("/users") interface Users { op listUsers(): User[]; // Defaults to GET /users op getUser(id: string): User; // Defaults to GET /users/{id} op createUser(@body user: User): User; // Defaults to POST /users } ``` The equivalent OpenAPI specification for the `Users` interface above would look like this: ```yaml filename="openapi.yaml" paths: /users: get: operationId: listUsers responses: 200: description: OK content: application/json: schema: type: array items: $ref: "#/components/schemas/User" post: operationId: createUser requestBody: required: true content: application/json: schema: $ref: "#/components/schemas/User" responses: 200: description: OK content: application/json: schema: $ref: "#/components/schemas/User" /users/{id}: get: operationId: getUser parameters: - name: id in: path required: true schema: type: string responses: 200: description: OK content: application/json: schema: $ref: "#/components/schemas/User" ``` ### Decorators in TypeSpec [Decorators](https://typespec.io/docs/language-basics/decorators) in TypeSpec add metadata to models, operations, and interfaces. They start with the `@` symbol followed by the decorator name. Here's an example of the `@doc` decorator: ```typescript filename="main.tsp" mark=1,3,6,9 @doc("A user in the system") model User { @doc("The unique identifier of the user") id: string; @doc("The name of the user") name: string; @doc("The email address of the user") email: string; } ``` Decorators allow you to add custom behavior to your TypeSpec definitions using JavaScript functions. You can [define your own decorators](https://typespec.io/docs/extending-typespec/create-decorators) or use built-in decorators provided by TypeSpec or third-party libraries. ### Learn More About TypeSpec The language features above should be enough to help you find your way around a TypeSpec specification. If you're interested in learning more about the TypeSpec language, see the [official documentation](https://typespec.io/docs/language-basics/overview). We'll cover more detailed examples of TypeSpec syntax in our full example below. ## Generating an OpenAPI Document from TypeSpec Now that we have a basic understanding of TypeSpec syntax, let's generate an OpenAPI document from a TypeSpec specification. The example below will guide you through the process of creating a TypeSpec project, writing a TypeSpec specification, and generating an OpenAPI document from it. For a speedrun, we've published the full example in a [GitHub repository](https://github.com/speakeasy-api/typespec-openapi-example). ### Step 1: Install the TypeSpec Compiler CLI Install `tsp` globally using npm: ```bash filename="Terminal" npm install -g @typespec/compiler ``` ### Step 2: Create a TypeSpec Project Create a new directory for your TypeSpec project and navigate into it: ```bash filename="Terminal" mkdir typespec-example-speakeasy cd typespec-example-speakeasy ``` Run the following command to initialize a new TypeSpec project: ```bash filename="Terminal" tsp init ``` This will prompt you to select a template for your project. Choose the `Generic REST API` template and press enter. Press enter repeatedly to select the defaults until the project is initialized. ### Step 3: Install the TypeSpec Dependencies Install the TypeSpec dependencies using `tsp`: ```bash filename="Terminal" tsp install ``` We'll need to install the `@typespec/versioning` and `@typespec/openapi` modules to generate an OpenAPI document. Run the following commands to install these modules: ```bash filename="Terminal" npm install @typespec/versioning @typespec/openapi ``` ### Step 4: Write Your TypeSpec Specification Open the `main.tsp` file in your text editor and write your TypeSpec specification. Here's an example of a simple TypeSpec specification: ### Example TypeSpec File Here's an example of a complete TypeSpec file for a Book Store API: ```typescript import "@typespec/http"; import "@typespec/openapi"; import "@typespec/openapi3"; import "@typespec/versioning"; using TypeSpec.Http; using TypeSpec.OpenAPI; using TypeSpec.Versioning; @service({ title: "Book Store API", }) @info({ termsOfService: "https://bookstore.example.com/terms", contact: { name: "API Support", url: "https://bookstore.example.com/support", email: "support@bookstore.example.com", }, license: { name: "Apache 2.0", url: "https://www.apache.org/licenses/LICENSE-2.0.html", }, }) @versioned(Versions) @server("http://127.0.0.1:4010", "Book Store API v1") @doc("API for managing a book store inventory and orders") namespace BookStore; enum Versions { `1.0.0`, } enum PublicationType { Book, Magazine, } @doc("Base model for books and magazines") model PublicationBase { @doc("Unique identifier") @key id: string; @doc("Title of the publication") title: string; @doc("Publication date") publishDate: utcDateTime; @doc("Price in USD") price: float32; @doc("Type of publication") type: PublicationType; } const BookExample1 = #{ id: "123", title: "Book Title", publishDate: utcDateTime.fromISO("2020-01-01T00:00:00Z"), price: 19.99, type: PublicationType.Book, author: "Author Name", isbn: "1234567890", }; const BookExample2 = #{ id: "456", title: "Another Book Title", publishDate: utcDateTime.fromISO("2020-02-01T00:00:00Z"), price: 24.99, type: PublicationType.Book, author: "Another Author", isbn: "0987654321", }; @example(BookExample1) @doc("Represents a book in the store") model Book extends PublicationBase { type: PublicationType.Book; @doc("Author of the book") author: string; @doc("ISBN of the book") isbn: string; } const MagazineExample1 = #{ id: "789", title: "Magazine Title", publishDate: utcDateTime.fromISO("2020-03-01T00:00:00Z"), price: 9.99, type: PublicationType.Magazine, issueNumber: 1, publisher: "Publisher Name", }; const MagazineExample2 = #{ id: "012", title: "Another Magazine Title", publishDate: utcDateTime.fromISO("2020-04-01T00:00:00Z"), price: 7.99, type: PublicationType.Magazine, issueNumber: 2, publisher: "Another Publisher", }; @example(MagazineExample1) @doc("Represents a magazine in the store") model Magazine extends PublicationBase { type: PublicationType.Magazine; @doc("Issue number of the magazine") issueNumber: int32; @doc("Publisher of the magazine") publisher: string; } const PublicationExample1 = BookExample1; const PublicationExample2 = MagazineExample1; @example(PublicationExample1) @discriminator("type") @oneOf union Publication { book: Book, magazine: Magazine, } @doc("Possible statuses for an order") enum OrderStatus { Pending, Shipped, Delivered, Cancelled, }; const OrderExample1 = #{ id: "abc", customerId: "123", items: #[BookExample1, MagazineExample1], totalPrice: 29.98, status: OrderStatus.Pending, }; @example(OrderExample1) @doc("Represents an order for publications") model Order { @doc("Unique identifier for the order") id: string; @doc("Customer who placed the order") customerId: string; @doc("List of publications in the order") items: Publication[]; @doc("Total price of the order") totalPrice: float32; @doc("Status of the order") status: OrderStatus; } @doc("Operations for managing publications") @tag("publications") @route("/publications") interface Publications { @opExample(#{ returnType: #[BookExample1, MagazineExample1] }) @doc("List all publications") @operationId("listPublications") list(): Publication[]; @opExample(#{ parameters: #{ id: "123" }, returnType: BookExample1 }) @doc("Get a specific publication by ID") @operationId("getPublication") get(@path id: string): Publication | Error; @opExample(#{ parameters: #{ publication: BookExample1 }, returnType: BookExample1, }) @doc("Create a new publication") @operationId("createPublication") create(@body publication: Publication): Publication | Error; } @doc("Operations for managing orders") @tag("orders") @route("/orders") interface Orders { @opExample(#{ parameters: #{ order: OrderExample1 }, returnType: OrderExample1, }) @doc("Place a new order") @operationId("placeOrder") placeOrder(@body order: Order): Order | Error; @opExample(#{ parameters: #{ id: "123" }, returnType: OrderExample1 }) @doc("Get an order by ID") @operationId("getOrder") getOrder(@path id: string): Order | Error; @opExample(#{ parameters: #{ id: "123", status: OrderStatus.Shipped }, returnType: OrderExample1, }) @doc("Update the status of an order") @operationId("updateOrderStatus") updateStatus(@path id: string, @body status: OrderStatus): Order | Error; } @example(#{ code: 404, message: "Publication not found" }) @error @doc("Error response") model Error { @doc("Error code") code: int32; @doc("Error message") message: string; } ``` Let's break down some of the key features of this TypeSpec file: #### Importing and Using Modules The file starts by importing necessary TypeSpec modules: ```typescript import "@typespec/http"; import "@typespec/openapi"; import "@typespec/openapi3"; import "@typespec/versioning"; using TypeSpec.Http; using TypeSpec.OpenAPI; using TypeSpec.Versioning; ``` These modules extend TypeSpec's capabilities for HTTP APIs, OpenAPI generation, and API versioning. #### Namespace and Service Definition The `BookStore` namespace is decorated with several metadata decorators: ```typescript @service({ title: "Book Store API", }) @info({ termsOfService: "https://bookstore.example.com/terms", contact: { name: "API Support", url: "https://bookstore.example.com/support", email: "support@bookstore.example.com", }, license: { name: "Apache 2.0", url: "https://www.apache.org/licenses/LICENSE-2.0.html", }, }) @versioned(Versions) @server("http://127.0.0.1:4010", "Book Store API v1") @doc("API for managing a book store inventory and orders") namespace BookStore; ``` - `@service` marks this namespace as a service and provides its title - `@info` provides additional information for the OpenAPI document - `@versioned` specifies the API versions - `@server` defines the base URL for the API - `@doc` provides a description for the API #### Models and Inheritance TypeSpec supports model inheritance, which is used to create specific publication types: ```typescript @doc("Base model for books and magazines") model PublicationBase { // properties } @example(BookExample1) @doc("Represents a book in the store") model Book extends PublicationBase { // additional properties specific to books } ``` #### Union Types Union types allow representing multiple possible types with a discriminator property: ```typescript @example(PublicationExample1) @discriminator("type") @oneOf union Publication { book: Book, magazine: Magazine, } ``` #### Interfaces and Operations Interfaces in TypeSpec group related operations: ```typescript @doc("Operations for managing publications") @tag("publications") @route("/publications") interface Publications { // operations } ``` Operations can be defined with various parameters and return types: ```typescript @opExample(#{ parameters: #{ id: "123" }, returnType: BookExample1 }) @doc("Get a specific publication by ID") @operationId("getPublication") get(@path id: string): Publication | Error; ``` The `@path` decorator indicates a path parameter, while `@body` would indicate a request body parameter. ### Step 5: Generate the OpenAPI Document Now that we've written our TypeSpec specification, we can generate an OpenAPI document from it using the `tsp` compiler. Run the following command to generate an OpenAPI document: ```bash filename="Terminal" tsp compile main.tsp --emit @typespec/openapi3 ``` The `tsp compile` command creates a new directory called `tsp-output`, then the `@typespec/openapi3` emitter creates the directories `@typespec/openapi3` within. If we were to use other emitters, such as protobuf, we would see `@typespec/protobuf` directories instead. Because we're using the versioning library, the OpenAPI document will be generated for the specified version of the API. In our case, the file generated by the OpenAPI 3 emitter will be named `openapi.yaml`. ### Step 6: View the Generated OpenAPI Document Open the generated OpenAPI document in your text editor or a YAML viewer to see the API specification. ### Generated OpenAPI Document Structure When the TypeSpec compiler processes our specification, it generates an OpenAPI document. Here's what the structure of the generated OpenAPI document looks like: #### OpenAPI Version The document starts with the OpenAPI version: ```yaml openapi: 3.0.0 ``` This is determined by the `@typespec/openapi3` emitter we used, which generates OpenAPI 3.0 documents. #### API Information The `info` section contains metadata from our `@service` and `@info` decorators: ```yaml info: title: Book Store API description: API for managing a book store inventory and orders termsOfService: https://bookstore.example.com/terms contact: name: API Support url: https://bookstore.example.com/support email: support@bookstore.example.com license: name: Apache 2.0 url: https://www.apache.org/licenses/LICENSE-2.0.html version: 1.0.0 ``` #### Server Information The server URL from our `@server` decorator: ```yaml servers: - url: http://127.0.0.1:4010 description: Book Store API v1 ``` #### Paths and Operations The `/publications` and `/orders` paths come from our interface route decorators: ```yaml paths: /publications: get: tags: - publications operationId: listPublications description: List all publications responses: "200": description: The request has succeeded. content: application/json: schema: type: array items: $ref: "#/components/schemas/Publication" # ... other operations ``` The `@body` parameters in TypeSpec translate to `requestBody` in OpenAPI: ```yaml /orders: post: tags: - orders operationId: placeOrder description: Place a new order requestBody: required: true content: application/json: schema: $ref: "#/components/schemas/Order" # ... responses ``` #### Components and Schemas Our models become schemas in the `components` section: ```yaml components: schemas: Book: allOf: - $ref: "#/components/schemas/PublicationBase" - type: object properties: author: description: Author of the book type: string isbn: description: ISBN of the book type: string required: - author - isbn ``` Union types use the `oneOf` keyword: ```yaml Publication: oneOf: - $ref: "#/components/schemas/Book" - $ref: "#/components/schemas/Magazine" discriminator: propertyName: type mapping: Book: "#/components/schemas/Book" Magazine: "#/components/schemas/Magazine" ``` ### Adding Retries with OpenAPI Extensions To add retry logic to the `listPublications` operation, we can add the Speakeasy `x-speakeasy-retries` extension to our TypeSpec specification: ```typescript interface Publications { @extension("x-speakeasy-retries", { strategy: "backoff", backoff: { initialInterval: 500, maxInterval: 60000, maxElapsedTime: 3600000, exponent: 1.5, }, statusCodes: ["5XX"], retryConnectionErrors: true }) @opExample(#{ returnType: #[BookExample1, MagazineExample1] }) @doc("List all publications") @operationId("listPublications") list(): Publication[]; // ... other operations } ``` This generates the following extension in the OpenAPI document: ```yaml paths: /publications: get: # ... other properties x-speakeasy-retries: strategy: backoff backoff: initialInterval: 500 maxInterval: 60000 maxElapsedTime: 3600000 exponent: 1.5 statusCodes: - 5XX retryConnectionErrors: true ``` ### Step 7: Generate an SDK from the OpenAPI Document Now that we have an OpenAPI document for our API, we can generate an SDK using Speakeasy. Make sure you have [Speakeasy installed](/docs/speakeasy-cli/getting-started): ```bash filename="Terminal" speakeasy --version ``` Then, generate a TypeScript SDK using the following command: ```bash filename="Terminal" speakeasy quickstart ``` This command generates a TypeScript SDK for the API defined in the OpenAPI document. The SDK will be placed in the `sdks/bookstore-ts` directory. ### Step 8: Customize the SDK We'd like to add retry logic to the SDK's `listPublications` to handle network errors gracefully. We'll do this by using an OpenAPI extension that [Speakeasy provides](/docs/customize-sdks/retries), `x-speakeasy-retries`. Instead of modifying the OpenAPI document directly, we'll add this extension to the TypeSpec specification and regenerate the OpenAPI document and SDK. After generating our OpenAPI document, we can generate an SDK using the Speakeasy CLI. Here's the process: 1. First, compile our TypeSpec code to generate the OpenAPI document: ```bash tsp compile main.tsp --emit @typespec/openapi3 ``` 2. Then use Speakeasy to generate the SDK: ```bash speakeasy quickstart ``` This will create a TypeScript SDK in the `./sdks/bookstore-ts` directory. Now that we've added the `x-speakeasy-retries` extension to the `listPublications` operation in the TypeSpec specification, we can use Speakeasy to recreate the SDK: ```bash filename="Terminal" speakeasy quickstart ``` ## Common TypeSpec Pitfalls and Possible Solutions While working with TypeSpec version 0.58.1, we encountered a few limitations and pitfalls that you should be aware of. ### 1. Limited Support for Model and Operation Examples Examples only shipped as part of TypeSpec version 0.58.0, and the OpenAPI emitter is still in development. This means that the examples provided in the TypeSpec specification may not be included in the generated OpenAPI document. To work around this limitation, you can provide examples directly in the OpenAPI document, preferably by using an [OpenAPI Overlay](/docs/prep-openapi/overlays/create-overlays). Here's an overlay, saved as `bookstore-overlay.yaml`, that adds examples to the `Book` and `Magazine` models in the OpenAPI document: ```yaml filename="bookstore-overlay.yaml" overlay: 1.0.0 info: title: Add Examples to Book and Magazine Models version: 1.0.0 actions: - target: $.components.schemas.Book update: example: id: "1" title: "The Great Gatsby" publishDate: "2022-01-01T00:00:00Z" price: 19.99 - target: $.components.schemas.Magazine update: example: id: "2" title: "National Geographic" publishDate: "2022-01-01T00:00:00Z" price: 5.99 ``` Validate the overlay using Speakeasy: ```bash filename="Terminal" speakeasy overlay validate -o bookstore-overlay.yaml ``` Then apply the overlay to the OpenAPI document: ```bash filename="Terminal" speakeasy overlay apply -s tsp-output/@typespec/openapi3/openapi.yaml -o bookstore-overlay.yaml > combined-openapi.yaml ``` If we look at the `combined-openapi.yaml` file, we should see the examples added to the `Book` and `Magazine` models, for example: ```yaml filename="combined-openapi.yaml" example: type: Magazine issueNumber: 1 publisher: Publisher Name id: "2" title: "National Geographic" publishDate: "2022-01-01T00:00:00Z" price: 5.99 ``` ### 2. Only Single Examples Supported At the time of writing, the OpenAPI emitter only supports a single example for each operation or model. If you provide multiple examples using the `@opExample` decorator in the TypeSpec specification, only the last example will be included in the OpenAPI document. OpenAPI version 3.0.0 introduced support for multiple examples using the `examples` field, and since OpenAPI 3.1.0, the singular `example` field is marked as deprecated in favor of multiple `examples`. ### 3. No Extensions at the Namespace Level We found that the `x-speakeasy-retries` extension could not be added at the namespace level in the TypeSpec specification, even though Speakeasy supports this extension at the operation level. The TypeSpec documentation on the [@extension](https://typespec.io/docs/libraries/openapi/reference/decorators#@TypeSpec.OpenAPI.extension) decorator does not mention any restrictions on where extensions can be applied, so this may be a bug or an undocumented limitation. To work around this limitation, you can add the `x-speakeasy-retries` extension directly to the OpenAPI document using an overlay, as shown in the previous example, or by adding it to each operation individually in the TypeSpec specification. ### 4. No Support for Webhooks or Callbacks TypeSpec does not yet support webhooks or callbacks, which are common in modern APIs. This means you cannot define webhook operations or callback URLs in your TypeSpec specification and generate OpenAPI documents for them. To work around this limitation, you can define webhooks and callbacks directly in the OpenAPI document using an overlay, or by adding them to the OpenAPI document manually. ### 5. OpenAPI 3.0.0 Only TypeSpec's OpenAPI emitter currently only supports OpenAPI version 3.0.0. We much prefer OpenAPI 3.1.0, which introduced several improvements over 3.0.0. ## The TypeSpec Playground To help you experiment with TypeSpec and see how it translates to OpenAPI, the Microsoft team created a [TypeSpec Playground](https://typespec.io/playground). We added our [TypeSpec specification](https://typespec.io/playground?e=%40typespec%2Fopenapi3&options=%7B%7D&c=aW1wb3J0ICJAdHlwZXNwZWMvaHR0cCI7CtIZb3BlbmFwadwcM9UddmVyc2lvbmluZyI7Cgp1c2luZyBUeXBlU3BlYy5IdHRwO9AVT3BlbkFQSdEYVslKOwoKQHNlcnZpY2UoewogIHRpdGxlOiAiQm9vayBTdG9yZSBBUEkiLAp9KQpAaW5mb8YmZXJtc09mU8Y5OiAi5ADtczovL2Jvb2tzxDYuZXhhbXBsZS5jb20vxS8iLAogIGNvbnRhY3Q6IMRGICBuYW3EPkFQSSBTdXDkAPDFJiAgdXJs31ZtL3PNMmVtYWnENMcWQNU0xSx9xAVsaWNlbnNl8ACJcGFjaGUgMi4w9QCId3d3LmHFIy5vcmcvx0RzL0xJQ0VOU0UtMi4wLmh0bWzIZ%2BQBNOcBtWVkKOcBdXMp5gFyZXIoxVk6Ly8xMjcuMC4wLjE6NDAxMCIs8AF%2FIHYxIikKQGRvYyjlASxmb3IgbWFuYWfkAdVhIOQA6yDlAOwgaW52ZW50b3J5IGFuZCBvcmRlcnMiKQrkAN9zcGFjZSDEWcVYOwoKZW51bSDoAJblAQlgMeQAiGAsCn3HHlB1YmxpY2F0aW9u5AI9xSXEQ%2BQA4U1hZ2F6aW5lxS7mAJ1CYXNlIG1vZGVs5QCk5QGE5QCKbccs5ACNxiDLWsU2xFrGRVVuaXF1ZSBpZGVudGlmaWVyIinEHGtleQogIGlkOiBzdHLmArrIMlTkArUgb2YgdGhlIHDKWcU55wLS0TXrAIEgZGF0ZcUtxT1zaERhdGU6IHV0Y8QJVGlt5AEuyThyaWPkAWkgVVNExjTEETogZmxvYXQzMssq5QEyb2byAJJ5cGU68AFbO%2BQBRmNvbnN05QFhReYCtTEgPSAj5AEb5AD4IjEyM%2BUCW%2B0DpeUA%2FcUX%2BAC9LmZyb21JU08oIjIwMjAtMDEtMDFUMDA6xQNaIinFPOYAxTE5Ljk5xWP0AKQu6AH7YXV0aG9y5AMxxQkgTmFt5gCDaXNibuYAqTQ1Njc4OeQDSH078wDZMu0A2TQ1Nu0A2UFub3RoZXLFMf8A4fQA4TL6AOEyNP8A4fAA4ecAiuYA6ewA5DA5ODc2NTQzMjHnAORA5wRpKOwBwOgDt1JlcHJlc2VudHPoA7FpbuUCr%2BUDuOkDFsQ7IGV4dGVuZHP1AyP6ALfrAnnnAZbnAxHEbuUCeOgA1fEDC0lTQk7RLuYA58gs6QKd6AP29QKhNzg57QHIyC3%2FAcT0AcQz%2BgHE%2FgKkyHTEImlzc3VlTnVtYmVyOiAxy3%2FEEOYEAsQM6ALA6gKq7wDm7gKuMDEy9QKu%2FwDu%2FADuNPoA7jf%2FAO73AO4yy3%2FEEOkApukA9vACt%2FAB2vUCu%2BgFyfYCv8hD%2FwLD%2FADJ7AKX5ADQIG7lANHoAp%2FoAIDnAqPrAPFpbu4FfuoB3tQ76wEa8QLj6wCb6wLm7APW6QIo0ivkAivwAUrsAWbTXeQBaWlzY3JpbWluYXRvcigi5AEj5Afyb25lT2YKdW7kBqTLOeUBR%2BQDqTrpB43oAN069AeXUG9zc2libOQBrmF0dXNlc%2BUHnmFu5gghIinmB%2BxPxA1TxSLFYVBlbmRpbmfEXlNoaXBwZWTEC0RlbGl2ZXLGDUNhbmNlbGzEDeoDPMVI9QQfYWJj5goGdXN0b21lcknsBtVpdGVtczogI1vsAWks8QFMXcQsdG90YWzlB4k6IDI5Ljk4xBXmAN067ADILukAxO0C6u0AovQC5%2BcBGuUBJ%2BsHvuoIvsVC%2FAi0xTvkAnboAWP3CLtD5wELIHdobyBwbGFjZWTPN%2BwBLdE%2FTGlzdO8IY3PoA5vLeuYBW%2BsCQltdy0VU5AFO5gRv6AMsyjzsAWvyCOPnAi%2FRNvMBjOUDYcY2T3BlcucArO0Ky%2B8Ba0B0YWcoItAVcm91dGUoIi%2FPGGludGVyZuQK4%2BsA5%2BYCyUDlBI7kCuYoIngtc3BlYWtlYXN5LXJldHJpZXMiLMUm5AC1cmF0ZWd5OiAiYmFja29mZucMBscO6AwtICBpbml0aWFsScR0dmFsOiA1MDDGK%2BQDinjKGDYwMM0aRWxhcHNlZOQGAzogM8UeyR9leHBvbmVudDogMS41xhXlDKvoAU5Db2RlczogWyI1WFgi5QMHICDkAMN5Q29ubmVj5QRScnJvcnM6IHRydWXkDIPlCzlvcOcC6ygje8QxdXLlBXL%2FA2vkA2vHQeoCamFsbPABpcVh5wHPSWQoImxpc3TsAYLFI8QVKCn1AoLtAJ5wYXJhbWV0ZeQAxCN76gr1IH0s7QC67Qsi6wClR2V0IGEg5A8taWZpY%2BwAqyBieSBJ5guw7gCwZ2XsAK%2FFIWdldChAcGF0aOsDs%2B4AvSB8IOUBa%2FIAw%2BUBlu8Ax%2BsAh%2FAAu%2BkBxvUA28Qe6wDeQ3JlYXRlIGEgbmV3zFXzANZjxSvwANnGFihAYm9kecxB7QDc%2BADq%2FwO%2BZ%2BoObuYDuMoP6AOyyRLqA6zlBCDnA6b%2FAUfGYOcEU%2FwBQs0h7wFDUOQFYucBQuoEru4BPOUFhsVFxR3KD%2BcBOewAiCnHCPoCEf8C1PcAt%2FEC1egGkvsCx%2BoAs8gN9ALG%2FwCv9AF56g589Qdk5wgd%2FwGL8QGLVXDkD3bnCkXoBkbrCJTwAOJ1xTDLecUkxhbGEfEA7ywg5gGy8wC28gEK5ALg6AghI3sgY29kZTogNDA0LCBtZXNzYWfkC%2BrsAxlub3QgZm91bmQi5ADWQGXERucDIsVSIHJlc3BvbnPqCx7GF%2BoINcYQxGnmA57FcvAKx8Yl5wCDxSjHDOwKuQ%3D%3D) to the playground. You can view the generated OpenAPI document and SDK, or browse a generated Swagger UI for the API. ## Further Reading This guide barely scratches the surface of what you can do with TypeSpec. This small language is evolving rapidly, and new features are being added all the time. Here are some resources to help you learn more about TypeSpec and how to use it effectively: - [TypeSpec Documentation](https://typespec.io/docs): The official TypeSpec documentation provides detailed information on the TypeSpec language, standard library, and emitters. - [TypeSpec Releases](https://github.com/microsoft/typespec/releases): Keep up with the latest TypeSpec releases and updates on GitHub. - [TypeSpec Playground](https://typespec.io/playground): Worth mentioning again: experiment with TypeSpec in the browser, generate OpenAPI documents, and view the resulting Swagger UI. - [Speakeasy Documentation](/docs): Speakeasy has extensive documentation on how to generate SDKs from OpenAPI documents, customize SDKs, and more. - [Speakeasy OpenAPI Reference](/openapi): For a detailed reference on the OpenAPI specification. # How to generate an OpenAPI document with Zod v3 Source: https://speakeasy.com/openapi/frameworks/zod-v3 import { Callout } from "@/mdx/components"; <Callout title="Zod Version" type="info"> This guide covers Zod v3. For Zod v4, see the [Zod v4 guide](/openapi/frameworks/zod). </Callout> Zod is a powerful and flexible schema validation library for TypeScript, which many developers use to define their TypeScript data parsing schemas. This tutorial demonstrates how to use another TypeScript library, the `zod-openapi` npm package, to convert Zod schemas into a complete OpenAPI document, and then how to use Speakeasy to generate a production-ready SDK from that document. ## Why use Zod with OpenAPI? Combining Zod with OpenAPI generation offers the best of both worlds: runtime validation and automatic API documentation. Instead of writing schemas twice – once for runtime validation and again for your OpenAPI document – you define your data models once in Zod and generate both TypeScript types and OpenAPI documentation from the same source. This eliminates the task of keeping hand-written OpenAPI documents in sync with your actual API implementation. When paired with Speakeasy's SDK generation, you get type-safe client libraries that automatically stay up to date with your API changes. <Callout title="Zod versions and other libraries" type="info"> This guide uses `zod-openapi`, which is actively maintained and offers better TypeScript integration than the older `zod-to-openapi` library. We show how to use a dual import strategy, which means we use both Zod v3 and v4 in the same project. This approach is necessary because `zod-openapi` currently requires Zod v3 compatibility, but you may want to use Zod v4 features elsewhere in your application. While Zod v4 introduces new features like `z.strictObject()` and `z.email()`, you'll need to use the standard Zod import for OpenAPI schemas and the `/v4` subpath for new features until `zod-openapi` adds full v4 support. Check the [zod-openapi releases](https://github.com/samchungy/zod-openapi/releases) for the latest compatibility updates. Unlike most libraries, Zod is directly embedded in hundreds of other libraries' public APIs. A normal `zod@4.0.0` release would force every one of those libraries to publish breaking changes simultaneously - a massive "version avalanche". The subpath approach (inspired by Go modules) lets libraries support both versions with one dependency, providing a gradual migration path for the entire ecosystem. See [Colin's detailed explanation](https://github.com/colinhacks/zod/issues/4371) for the full technical reasoning. </Callout> ## zod-openapi overview The [`zod-openapi`](https://github.com/samchungy/zod-openapi) package is a TypeScript library that helps developers define OpenAPI schemas as Zod schemas. The stated goal of the project is to cut down on code duplication, and it does a wonderful job of this. Zod schemas map well to OpenAPI schemas, and the changes required to extract OpenAPI documents from a schema defined in Zod are often small. <Callout title="Migrating from zod-to-openapi?" type="info"> If you're currently using the older `zod-to-openapi` library, the syntax will be familiar, and you can use either library. For migration guidance, see the [zod-openapi migration documentation](https://github.com/samchungy/zod-openapi#migration). </Callout> ### Key concepts Here's an overview of some key concepts in Zod and how they relate to Zod versioning. #### Schemas and z.strictObject Zod v4 introduces top-level helpers like `z.strictObject()` for objects that reject unknown keys and `z.email()` for email validation. ```typescript filename="concept.ts" import { z as z3 } from "zod"; import { z as z4 } from "zod/v4"; // Use z4 for new Zod v4 features const user = z4.strictObject({ email: z4.email(), age: z4.number().int().min(18), }); ``` #### Field metadata The `.openapi()` method adds OpenAPI-specific metadata like descriptions and examples to any Zod schema. Use `z3` for OpenAPI schemas. ```typescript filename="concept.ts" import { z as z3 } from "zod"; import { extendZodWithOpenApi } from "zod-openapi"; extendZodWithOpenApi(z3); const name = z3.string().min(1).openapi({ description: "The user's full name", example: "Alice Johnson", }); ``` #### Operation objects Use `ZodOpenApiOperationObject` to define API endpoints with request and response schemas. ```typescript filename="concept.ts" import { z as z3 } from "zod"; import { ZodOpenApiOperationObject } from "zod-openapi"; const getUser: ZodOpenApiOperationObject = { operationId: "getUser", summary: "Get user by ID", requestParams: { path: z3.object({ id: z3.string() }) }, responses: { "200": { description: "User found", content: { "application/json": { schema: userSchema } }, }, }, }; ``` #### Adding tags to operations Tags help organize your API operations in the generated documentation and SDKs. You can add a `tags` array to each operation object: ```typescript filename="concept.ts" const getBurger: ZodOpenApiOperationObject = { operationId: "getBurger", summary: "Get a burger by ID", tags: ["burgers"], // <--- Add tags here // ...rest of the operation }; ``` #### Using Zod v4 features alongside OpenAPI documents While your OpenAPI documents must use the `z3` instance for compatibility, you can use `z4` features for internal validation, type checking, or other parts of your application: ```typescript filename="concept.ts" // OpenAPI-compatible schemas (use z3) const apiUserSchema = z3 .object({ id: z3.string(), name: z3.string(), email: z3.string(), }) .openapi("User"); // Internal schemas can use Zod v4 features (use z4) const internalUserSchema = z4.strictObject({ // v4 feature id: z4.string().uuid(), name: z4.string().min(1), email: z4.string().email(), // v4 feature preferences: z4 .object({ darkMode: z4.boolean(), notifications: z4.enum(["all", "mentions", "none"]), }) .optional(), }); ``` ## Step-by-step tutorial: From Zod to OpenAPI to an SDK Now let's walk through the process of generating an OpenAPI document and SDK for our Burgers and Orders API. ### Requirements This tutorial assumes basic familiarity with TypeScript and Node.js development. The following should be installed on your machine: - [Node.js version 18 or above](https://nodejs.org/en/download) - The [Speakeasy CLI](/docs/introduction#install-the-speakeasy-cli), which we'll use to generate an SDK from the OpenAPI document ### Creating your Zod project <Callout title="Complete Example Available" type="info"> The source code for our complete example is available in the [`speakeasy-api/examples`](https://github.com/speakeasy-api/examples.git) repository in the `zod-openapi` directory. The project contains a pre-generated Python SDK with instructions on how to generate more SDKs. You can clone this repository to test how changes to the Zod schema definition result in changes to the generated SDK. </Callout> Alternatively, you can initialize a new npm project and install the required dependencies if you're not using our burgers example. ``` npm init -y npm install zod@^3.25 zod-openapi yaml ``` If you're following along, start by cloning the `speakeasy-api/examples` repository. ```bash filename="Terminal" git clone https://github.com/speakeasy-api/examples.git cd zod-openapi ``` Next, install the dependencies: ```bash filename="Terminal" npm install ``` ### Installing TypeScript development tools For this tutorial, we'll use `tsx` for running TypeScript directly: ```bash filename="Terminal" npm install -D tsx ``` ### Creating your app's first Zod schema Save this TypeScript code in a new file called `index.ts`. Note the dual import strategy: ```typescript filename="index.ts" // Import Zod v3 compatible instance for zod-openapi import { z as z3 } from "zod"; // Import Zod v4 for new features and future migration import { z as z4 } from "zod/v4"; // For now, we'll use z3 for OpenAPI schemas since zod-openapi requires it const burgerSchema = z3.object({ id: z3.number().min(1), name: z3.string().min(1).max(50), description: z3.string().max(255).optional(), }); ``` ### Extending Zod with OpenAPI We'll add the `openapi` method to Zod by calling `extendZodWithOpenApi` once. Update `index.ts` to import `extendZodWithOpenApi` from `zod-openapi`, then call `extendZodWithOpenApi` on the `z3` instance. ```typescript filename="index.ts" import { z as z3 } from "zod"; import { extendZodWithOpenApi } from "zod-openapi"; import { z as z4 } from "zod/v4"; // Extend the Zod v3 compatible instance for zod-openapi extendZodWithOpenApi(z3); // Schemas defined with z3 for current zod-openapi compatibility const burgerSchema = z3.object({ id: z3.number().min(1), name: z3.string().min(1).max(50), description: z3.string().max(255).optional(), }); ``` ### Registering and generating a component schema Next, we'll use the new `openapi` method provided by `extendZodWithOpenApi` to register an OpenAPI schema for the `burgerSchema`. Edit `index.ts` and add `.openapi({ref: "Burger"}` to the `burgerSchema` schema object. We'll also add an OpenAPI generator, `OpenApiGeneratorV31`, and log the generated component to the console as YAML. ```typescript filename="index.ts" import { z as z3 } from "zod"; import { extendZodWithOpenApi } from "zod-openapi"; import { z as z4 } from "zod/v4"; extendZodWithOpenApi(z3); const burgerSchema = z3.object({ id: z3.number().min(1), name: z3.string().min(1).max(50), description: z3.string().max(255).optional(), }); burgerSchema.openapi({ ref: "Burger" }); ``` ### Adding metadata to components To generate an SDK that offers a great developer experience, we recommend adding descriptions and examples to all fields in OpenAPI components. With `zod-openapi`, we'll call the `.openapi` method on each field, and add an example and description to each field. We'll also add a description to the `Burger` component itself. Edit `index.ts` and edit `burgerSchema` to add OpenAPI metadata. ```typescript filename="index.ts" import { z as z3 } from "zod"; import { extendZodWithOpenApi } from "zod-openapi"; import { z as z4 } from "zod/v4"; extendZodWithOpenApi(z3); const burgerSchema = z3.object({ id: z3.number().min(1).openapi({ description: "The unique identifier of the burger.", example: 1, }), name: z3.string().min(1).max(50).openapi({ description: "The name of the burger.", example: "Veggie Burger", }), description: z3.string().max(255).optional().openapi({ description: "The description of the burger.", example: "A delicious bean burger with avocado.", }), }); burgerSchema.openapi({ ref: "Burger", description: "A burger served at the restaurant.", }); ``` ### Preparing to generate an OpenAPI document Now that we know how to register components with metadata for our OpenAPI schema, let's generate a complete schema document. Import `yaml` and `createDocument`. ```typescript filename="index.ts" import * as yaml from "yaml"; import { z as z3 } from "zod"; import { createDocument, extendZodWithOpenApi } from "zod-openapi"; import { z as z4 } from "zod/v4"; extendZodWithOpenApi(z3); const burgerSchema = z3.object({ id: z3.number().min(1).openapi({ description: "The unique identifier of the burger.", example: 1, }), name: z3.string().min(1).max(50).openapi({ description: "The name of the burger.", example: "Veggie Burger", }), description: z3.string().max(255).optional().openapi({ description: "The description of the burger.", example: "A delicious bean burger with avocado.", }), }); burgerSchema.openapi({ ref: "Burger", description: "A burger served at the restaurant.", }); ``` ### Generating an OpenAPI document We'll use the `createDocument` method to generate an OpenAPI document. We'll pass in the `burgerSchema` and a title for the document. ```typescript filename="index.ts" import * as yaml from "yaml"; import { z as z3 } from "zod"; import { createDocument, extendZodWithOpenApi } from "zod-openapi"; import { z as z4 } from "zod/v4"; extendZodWithOpenApi(z3); const burgerSchema = z3.object({ id: z3.number().min(1).openapi({ description: "The unique identifier of the burger.", example: 1, }), name: z3.string().min(1).max(50).openapi({ description: "The name of the burger.", example: "Veggie Burger", }), description: z3.string().max(255).optional().openapi({ description: "The description of the burger.", example: "A delicious bean burger with avocado.", }), }); burgerSchema.openapi({ ref: "Burger", description: "A burger served at the restaurant.", }); const document = createDocument({ openapi: "3.1.0", info: { title: "Burger Restaurant API", description: "An API for managing burgers and orders at a restaurant.", version: "1.0.0", }, servers: [ { url: "https://example.com", description: "The production server.", }, ], components: { schemas: { burgerSchema, }, }, }); console.log(yaml.stringify(document)); ``` ### Adding a burger ID schema To make the burger ID available to other schemas, we'll define a burger ID schema. We'll also use this schema to define a path parameter for the burger ID later on. ```typescript filename="index.ts" const BurgerIdSchema = z3 .number() .min(1) .openapi({ ref: "BurgerId", description: "The unique identifier of the burger.", example: 1, param: { in: "path", name: "id", }, }); ``` Update the `burgerSchema` to use the `BurgerIdSchema`. ```typescript filename="index.ts" const burgerSchema = z3.object({ id: BurgerIdSchema, name: z3.string().min(1).max(50).openapi({ description: "The name of the burger.", example: "Veggie Burger", }), description: z3.string().max(255).optional().openapi({ description: "The description of the burger.", example: "A delicious bean burger with avocado.", }), }); ``` ### Adding a schema for creating burgers We'll add a schema for creating burgers that doesn't include an ID. We'll use this schema to define the request body for the create burger path. ```typescript filename="index.ts" const burgerCreateSchema = burgerSchema.omit({ id: true }).openapi({ ref: "BurgerCreate", description: "A burger to create.", }); ``` ### Adding order schemas To match the final OpenAPI output, let's add schemas and endpoints for orders. ```typescript filename="index.ts" const OrderIdSchema = z3 .number() .min(1) .openapi({ ref: "OrderId", description: "The unique identifier of the order.", example: 1, param: { in: "path", name: "id", }, }); const orderSchema = z3 .object({ id: OrderIdSchema, burger_ids: z3 .array(BurgerIdSchema) .min(1) .openapi({ description: "The burgers in the order.", example: [1, 2], }), time: z3.string().openapi({ description: "The time the order was placed.", example: "2021-01-01T00:00:00.000Z", format: "date-time", }), table: z3.number().min(1).openapi({ description: "The table the order is for.", example: 1, }), status: z3.enum(["pending", "in_progress", "ready", "delivered"]).openapi({ description: "The status of the order.", example: "pending", }), note: z3.string().optional().openapi({ description: "A note for the order.", example: "No onions.", }), }) .openapi({ ref: "Order", description: "An order placed at the restaurant.", }); const orderCreateSchema = orderSchema.omit({ id: true }).openapi({ ref: "OrderCreate", description: "An order to create.", }); ``` ### Defining burger and order operations Now, define the operations for creating and getting burgers and orders, and listing burgers: ```typescript filename="index.ts" import { ZodOpenApiOperationObject } from "zod-openapi"; const createBurger: ZodOpenApiOperationObject = { operationId: "createBurger", summary: "Create a new burger", description: "Creates a new burger in the database.", tags: ["burgers"], requestBody: { description: "The burger to create.", content: { "application/json": { schema: burgerCreateSchema, }, }, }, responses: { "201": { description: "The burger was created successfully.", content: { "application/json": { schema: burgerSchema, }, }, }, }, }; const getBurger: ZodOpenApiOperationObject = { operationId: "getBurger", summary: "Get a burger", description: "Gets a burger from the database.", tags: ["burgers"], requestParams: { path: z3.object({ id: BurgerIdSchema }), }, responses: { "200": { description: "The burger was retrieved successfully.", content: { "application/json": { schema: burgerSchema, }, }, }, }, }; const listBurgers: ZodOpenApiOperationObject = { operationId: "listBurgers", summary: "List burgers", description: "Lists all burgers in the database.", tags: ["burgers"], responses: { "200": { description: "The burgers were retrieved successfully.", content: { "application/json": { schema: z3.array(burgerSchema), }, }, }, }, }; const createOrder: ZodOpenApiOperationObject = { operationId: "createOrder", summary: "Create a new order", description: "Creates a new order in the database.", tags: ["orders"], requestBody: { description: "The order to create.", content: { "application/json": { schema: orderCreateSchema, }, }, }, responses: { "201": { description: "The order was created successfully.", content: { "application/json": { schema: orderSchema, }, }, }, }, }; const getOrder: ZodOpenApiOperationObject = { operationId: "getOrder", summary: "Get an order", description: "Gets an order from the database.", tags: ["orders"], requestParams: { path: z3.object({ id: OrderIdSchema }), }, responses: { "200": { description: "The order was retrieved successfully.", content: { "application/json": { schema: orderSchema, }, }, }, }, }; ``` ### Adding a webhook that runs when a burger is created We'll add a webhook that runs when a burger is created. We'll use the `ZodOpenApiOperationObject` type to define the webhook. ```typescript filename="index.ts" const createBurgerWebhook: ZodOpenApiOperationObject = { operationId: "createBurgerWebhook", summary: "New burger webhook", description: "A webhook that is called when a new burger is created.", tags: ["burgers"], requestBody: { description: "The burger that was created.", content: { "application/json": { schema: burgerSchema, }, }, }, responses: { "200": { description: "The webhook was processed successfully.", }, }, }; ``` ### Registering all paths, webhooks, and extensions Now, register all schemas, paths, webhooks, and the `x-speakeasy-retries` extension: ```typescript filename="index.ts" const document = createDocument({ openapi: "3.1.0", info: { title: "Burger Restaurant API", description: "An API for managing burgers and orders at a restaurant.", version: "1.0.0", }, servers: [ { url: "https://example.com", description: "The production server.", }, ], "x-speakeasy-retries": { strategy: "backoff", backoff: { initialInterval: 500, maxInterval: 60000, maxElapsedTime: 3600000, exponent: 1.5, }, statusCodes: ["5XX"], retryConnectionErrors: true, }, paths: { "/burgers": { post: createBurger, get: listBurgers, }, "/burgers/{id}": { get: getBurger, }, "/orders": { post: createOrder, }, "/orders/{id}": { get: getOrder, }, }, webhooks: { "/burgers": { post: createBurgerWebhook, }, }, components: { schemas: { burgerSchema, burgerCreateSchema, BurgerIdSchema, orderSchema, orderCreateSchema, OrderIdSchema, }, }, }); console.log(yaml.stringify(document)); ``` Speakeasy will read the `x-speakeasy-*` extensions to configure the SDK. In this example, the `x-speakeasy-retries` extension will configure the SDK to retry failed requests. For more information on the available extensions, see the [extensions guide](/openapi/extensions). ### Generating the OpenAPI document Run the `index.ts` file to generate the OpenAPI document. ```bash filename="Terminal" npx tsx index.ts > openapi.yaml ``` The output will be a YAML file that looks like this: ```yaml openapi: 3.1.0 info: title: Burger Restaurant API description: An API for managing burgers and orders at a restaurant. version: 1.0.0 servers: - url: https://example.com description: The production server. x-speakeasy-retries: strategy: backoff backoff: initialInterval: 500 maxInterval: 60000 maxElapsedTime: 3600000 exponent: 1.5 statusCodes: - 5XX retryConnectionErrors: true paths: /burgers: post: operationId: createBurger summary: Create a new burger description: Creates a new burger in the database. tags: - burgers requestBody: description: The burger to create. content: application/json: schema: $ref: "#/components/schemas/BurgerCreate" responses: "201": description: The burger was created successfully. content: application/json: schema: $ref: "#/components/schemas/burgerSchema" get: operationId: listBurgers summary: List burgers description: Lists all burgers in the database. tags: - burgers responses: "200": description: The burgers were retrieved successfully. content: application/json: schema: type: array items: $ref: "#/components/schemas/burgerSchema" /burgers/{id}: get: operationId: getBurger summary: Get a burger description: Gets a burger from the database. tags: - burgers parameters: - in: path name: id description: The unique identifier of the burger. schema: $ref: "#/components/schemas/BurgerId" required: true responses: "200": description: The burger was retrieved successfully. content: application/json: schema: $ref: "#/components/schemas/burgerSchema" /orders: post: operationId: createOrder summary: Create a new order description: Creates a new order in the database. tags: - orders requestBody: description: The order to create. content: application/json: schema: $ref: "#/components/schemas/OrderCreate" responses: "201": description: The order was created successfully. content: application/json: schema: $ref: "#/components/schemas/Order" /orders/{id}: get: operationId: getOrder summary: Get an order description: Gets an order from the database. tags: - orders parameters: - in: path name: id description: The unique identifier of the order. schema: $ref: "#/components/schemas/OrderId" required: true responses: "200": description: The order was retrieved successfully. content: application/json: schema: $ref: "#/components/schemas/Order" webhooks: /burgers: post: operationId: createBurgerWebhook summary: New burger webhook description: A webhook that is called when a new burger is created. tags: - burgers requestBody: description: The burger that was created. content: application/json: schema: $ref: "#/components/schemas/burgerSchema" responses: "200": description: The webhook was processed successfully. components: schemas: burgerSchema: type: object properties: id: $ref: "#/components/schemas/BurgerId" name: type: string minLength: 1 maxLength: 50 description: The name of the burger. example: Veggie Burger description: type: string maxLength: 255 description: The description of the burger. example: A delicious bean burger with avocado. required: - id - name BurgerCreate: type: object properties: name: type: string minLength: 1 maxLength: 50 description: The name of the burger. example: Veggie Burger description: type: string maxLength: 255 description: The description of the burger. example: A delicious bean burger with avocado. required: - name description: A burger to create. BurgerId: type: number minimum: 1 description: The unique identifier of the burger. example: 1 Order: type: object properties: id: $ref: "#/components/schemas/OrderId" burger_ids: type: array items: $ref: "#/components/schemas/BurgerId" minItems: 1 description: The burgers in the order. example: &a1 - 1 - 2 time: type: string format: date-time description: The time the order was placed. example: 2021-01-01T00:00:00.000Z table: type: number minimum: 1 description: The table the order is for. example: 1 status: type: string enum: &a2 - pending - in_progress - ready - delivered description: The status of the order. example: pending note: type: string description: A note for the order. example: No onions. required: - id - burger_ids - time - table - status description: An order placed at the restaurant. OrderCreate: type: object properties: burger_ids: type: array items: $ref: "#/components/schemas/BurgerId" minItems: 1 description: The burgers in the order. example: *a1 time: type: string format: date-time description: The time the order was placed. example: 2021-01-01T00:00:00.000Z table: type: number minimum: 1 description: The table the order is for. example: 1 status: type: string enum: *a2 description: The status of the order. example: pending note: type: string description: A note for the order. example: No onions. required: - burger_ids - time - table - status description: An order to create. OrderId: type: number minimum: 1 description: The unique identifier of the order. example: 1 ``` ### Generating an SDK With our OpenAPI document complete, we can now generate an SDK using the Speakeasy SDK generator. #### Installing the Speakeasy CLI First, install the Speakeasy CLI: ```bash filename="Terminal" # Using Homebrew (recommended) brew install speakeasy-api/tap/speakeasy # Using curl curl -fsSL https://go.speakeasy.com/cli-install.sh | sh ``` #### Linting your OpenAPI document Before generating SDKs, lint your OpenAPI document to catch common issues: ```bash filename="Terminal" speakeasy lint openapi --schema openapi.yaml ``` #### Using AI to improve your OpenAPI document The Speakeasy CLI now includes AI-powered suggestions to automatically improve your OpenAPI documents: ```bash filename="Terminal" speakeasy suggest openapi.yaml ``` Follow the onscreen prompts to provide the necessary configuration details for your new SDK, such as the schema and output path. Read the [Speakeasy Suggest](/docs/prep-openapi/maintenance) documentation for more information on how to use Speakeasy Suggest. #### Generating your SDK Now you can generate your SDK using the quickstart command: ```bash filename="Terminal" speakeasy quickstart ``` Follow the onscreen prompts to provide the necessary configuration details for your new SDK, such as the name, schema location, and output path. Enter `openapi.yaml` (or your improved OpenAPI document if you used suggestions) when prompted for the OpenAPI document location, and select your preferred language when prompted. ## Using your generated SDK Once you've generated your SDK, you can [publish](/docs/publish-sdk) it for use. For TypeScript, you can publish it as an npm package. TypeScript SDKs generated with Speakeasy include an installable [Model Context Protocol (MCP) server](/docs/standalone-mcp/build-server) where the various SDK methods are exposed as tools that AI applications can invoke. Your SDK documentation includes instructions for installing the MCP server. <Callout title="Production Readiness" type="warning"> Note that the SDK is not ready for production use immediately after generation. To get it production-ready, follow the steps outlined in your Speakeasy workspace. </Callout> ### Adding SDK generation to your CI/CD pipeline The Speakeasy [`sdk-generation-action`](https://github.com/speakeasy-api/sdk-generation-action) repository provides workflows for integrating the Speakeasy CLI into CI/CD pipelines to automatically regenerate SDKs when your Zod schemas change. You can set up Speakeasy to automatically push a new branch to your SDK repositories so that your engineers can review and merge the SDK changes. For an overview of how to set up automation for your SDKs, see the Speakeasy [SDK workflow syntax reference](/docs/speakeasy-reference/workflow-file). ## Summary In this tutorial, we learned how to generate OpenAPI schemas from Zod and create client SDKs with Speakeasy. By following these steps, you can ensure that your API is well-documented, easy to use, and offers a great developer experience. ### Further reading This guide covered the basics of generating an OpenAPI document using `zod-openapi`. Here are some resources to help you learn more about Zod, OpenAPI, and Speakeasy: - [The `zod-openapi` documentation](https://github.com/samchungy/zod-openapi): Learn more about the `zod-openapi` library, including advanced features like custom serializers and middleware integration. - [The Zod documentation](https://zod.dev/): Comprehensive guide to Zod schema validation, including the latest v4 features. # How to generate an OpenAPI document with Zod v4 Source: https://speakeasy.com/openapi/frameworks/zod import { Callout } from "@/mdx/components"; <Callout title="Zod Version" type="info"> This guide covers Zod v4. For Zod v3, see the [Zod v3 guide](/openapi/frameworks/zod-v3). </Callout> Zod is a powerful and flexible schema validation library for TypeScript, which many developers use to define their TypeScript data parsing schemas. This tutorial demonstrates how to use another TypeScript library, the zod-openapi NPM package, to convert Zod schemas into a complete OpenAPI document, and then how to use Speakeasy to generate a production-ready SDK from that document. ## Why use Zod with OpenAPI? Combining Zod with OpenAPI generation offers the best of both worlds: runtime validation and automatic API documentation. Instead of writing schemas twice - once for runtime validation and again for your OpenAPI document - you define your data models once in Zod and generate both TypeScript types and OpenAPI documentation from the same source. This eliminates the task of keeping hand-written OpenAPI documents in sync with your actual API implementation. When paired with Speakeasy's SDK generation, you get type-safe client libraries that automatically stay up to date with your API changes. ## Step-by-step tutorial: From Zod to OpenAPI to SDK Now let's walk through the process of generating an OpenAPI document and SDK for our Burgers and Orders API. ### Requirements This tutorial assumes basic familiarity with TypeScript and Node.js development. The following should be installed on your machine: - [Node.js version 20 or above](https://nodejs.org/en/download). - The [Speakeasy CLI](/docs/introduction#install-the-speakeasy-cli), which we'll use to generate an SDK from the OpenAPI document. ### Create a Zod project The source code for our complete example is available in the [`speakeasy-api/examples`](https://github.com/speakeasy-api/examples.git) repository in the `zod-openapi` directory. The project contains a pre-generated Python SDK with instructions on how to generate more SDKs. You can clone this repository to test how changes to the Zod schema definition result in changes to the generated SDK. Start by cloning the `speakeasy-api/examples` repository. ```bash filename="Terminal" git clone https://github.com/speakeasy-api/examples.git cd zod-openapi npm install ``` <Callout title="Alternative Setup" type="info"> Alternatively, initialize a new NPM project and install the required dependencies, and try to implement the suggested steps in this tutorial: ``` npm init -y npm install zod@^4.0.0 yaml zod-openapi ``` </Callout> ### Installing TypeScript development tools For this tutorial, we'll use `tsx` for running TypeScript directly: ```bash filename="Terminal" npm install -D tsx ``` ### Create the first Zod schema Save this TypeScript code in a new file called `index.ts`. Note the dual import strategy: ```typescript filename="index.ts" import zod from "zod"; const burgerSchema = zod.object({ id: zod.number().min(1), name: zod.string().min(1).max(50), description: zod.string().max(255).optional(), }); ``` ### Extending Zod with OpenAPI ```typescript filename="index.ts" const burgerSchema = zod .object({ id: zod.number().min(1).meta({ description: "The unique identifier of the burger.", example: 1, }), name: zod.string().min(1).max(50).meta({ description: "The name of the burger.", example: "Veggie Burger", }), description: zod.string().max(255).optional().meta({ description: "The description of the burger.", example: "A delicious bean burger with avocado.", }), }) .meta({ description: "A burger served at the restaurant.", }); ``` ### Reusing schemas with references To avoid duplication and promote reuse, we can define reusable schemas for common fields. For example, we can define a `BurgerIdSchema` for the burger ID field and use it in the `burgerSchema`. ```typescript filename="index.ts" // Define a reusable BurgerId schema const BurgerIdSchema = zod.number().min(1).meta({ description: "The unique identifier of the burger.", example: 1, readOnly: true, }); const burgerSchema = zod .object({ id: BurgerIdSchema, // Use the BurgerIdSchema name: zod.string().min(1).max(50).meta({ description: "The name of the burger.", example: "Veggie Burger", }), description: zod.string().max(255).optional().meta({ description: "The description of the burger.", example: "A delicious bean burger with avocado.", }), }) .meta({ description: "A burger served at the restaurant.", }); ``` ### Generating an OpenAPI document Now that the Zod schemas are defined with OpenAPI metadata, it's time to generate an OpenAPI document. For this two imports are needed from the `zod-openapi` package: `ZodOpenApiOperationObject` and `createDocument`. ```typescript filename="index.ts" import { ZodOpenApiOperationObject, createDocument } from "zod-openapi"; ``` The `createDocument` method will help generate an OpenAPI document. Pass in the `burgerSchema` and a title for the document. ```typescript filename="index.ts" const document = createDocument({ openapi: "3.1.0", info: { title: "Burger Restaurant API", description: "An API for managing burgers and orders at a restaurant.", version: "1.0.0", }, servers: [ { url: "https://example.com", description: "The production server.", }, ], components: { schemas: { burgerSchema, }, }, }); console.log(yaml.stringify(document)); ``` ### Varying read/write schemas One common pattern in OpenAPI documents is to have separate schemas for creating and updating resources. This allows you to define different validation rules for these operations. That would look sometime like this: ```typescript filename="index.ts" const burgerCreateSchema = burgerSchema.omit({ id: true }).meta({ description: "A burger to create.", }); ``` An easier approach is to utilize `readOnly` and `writeOnly` properties in OpenAPI. Marking the `id` field as `readOnly` indicates that it is only returned in responses and not expected in requests. ```typescript filename="index.ts" const BurgerIdSchema = zod.number().min(1).meta({ description: "The unique identifier of the burger.", example: 1, readOnly: true, }); ``` This way, we can use the same schema for both creating and retrieving burgers. ### More advanced schemas Let's define a more complex schema for orders, which includes an array of burger IDs, timestamps, and status fields. ```typescript filename="index.ts" const OrderIdSchema = zod.number().min(1).meta({ description: "The unique identifier of the order.", example: 1, readOnly: true, }); const orderStatusEnum = zod.enum([ "pending", "in_progress", "ready", "delivered", ]); const orderSchema = zod .object({ id: OrderIdSchema, burger_ids: zod .array(BurgerIdSchema) .nonempty() .meta({ description: "The burgers in the order.", example: [1, 2], }), time: zod.iso.datetime().meta({ description: "The time the order was placed.", example: "2021-01-01T00:00:00.000Z", }), table: zod.number().min(1).meta({ description: "The table the order is for.", example: 1, }), status: orderStatusEnum.meta({ description: "The status of the order.", example: "pending", }), note: zod.string().optional().meta({ description: "A note for the order.", example: "No onions.", }), }) .meta({ description: "An order placed at the restaurant.", }); ``` ### Defining operations Operations need to be defined before they can be registered in the OpenAPI document. Define an operation for creating and getting burgers and orders, and listing burgers: ```typescript filename="index.ts" import { ZodOpenApiOperationObject } from "zod-openapi"; const createBurger: ZodOpenApiOperationObject = { operationId: "createBurger", summary: "Create a new burger", description: "Creates a new burger in the database.", tags: ["burgers"], requestBody: { description: "The burger to create.", content: { "application/json": { schema: burgerSchema, }, }, }, responses: { "201": { description: "The burger was created successfully.", content: { "application/json": { schema: burgerSchema, }, }, }, }, }; const getBurger: ZodOpenApiOperationObject = { operationId: "getBurger", summary: "Get a burger", description: "Gets a burger from the database.", tags: ["burgers"], requestParams: { path: zod.object({ id: BurgerIdSchema }), }, responses: { "200": { description: "The burger was retrieved successfully.", content: { "application/json": { schema: burgerSchema, }, }, }, }, }; const listBurgers: ZodOpenApiOperationObject = { operationId: "listBurgers", summary: "List burgers", description: "Lists all burgers in the database.", tags: ["burgers"], responses: { "200": { description: "The burgers were retrieved successfully.", content: { "application/json": { schema: zod.array(burgerSchema), }, }, }, }, }; // Order operations const createOrder: ZodOpenApiOperationObject = { operationId: "createOrder", summary: "Create a new order", description: "Creates a new order in the database.", tags: ["orders"], requestBody: { description: "The order to create.", content: { "application/json": { schema: orderSchema, }, }, }, responses: { "201": { description: "The order was created successfully.", content: { "application/json": { schema: orderSchema, }, }, }, }, }; const getOrder: ZodOpenApiOperationObject = { operationId: "getOrder", summary: "Get an order", description: "Gets an order from the database.", tags: ["orders"], requestParams: { path: zod.object({ id: OrderIdSchema }), }, responses: { "200": { description: "The order was retrieved successfully.", content: { "application/json": { schema: orderSchema, }, }, }, }, }; ``` ### Adding a webhook that runs when a burger is created Webhooks are like operations that runs when a server-side action is triggered, e.g. when a burger has been created. They're similar enough that zod-openapi uses the same `ZodOpenApiOperationObject` type to define the webhook. ```typescript filename="index.ts" const createBurgerWebhook: ZodOpenApiOperationObject = { operationId: "createBurgerWebhook", summary: "New burger webhook", description: "A webhook that is called when a new burger is created.", tags: ["burgers"], requestBody: { description: "The burger that was created.", content: { "application/json": { schema: burgerSchema, }, }, }, responses: { "200": { description: "The webhook was processed successfully.", }, }, }; ``` ### Registering all paths, webhooks, and extensions Now, register all schemas, paths, webhooks, and the `x-speakeasy-retries` extension: ```typescript filename="index.ts" const document = createDocument({ openapi: "3.1.0", info: { title: "Burger Restaurant API", description: "An API for managing burgers and orders at a restaurant.", version: "1.0.0", }, servers: [ { url: "https://example.com", description: "The production server.", }, ], paths: { "/burgers": { post: createBurger, get: listBurgers, }, "/burgers/{id}": { get: getBurger, }, "/orders": { post: createOrder, }, "/orders/{id}": { get: getOrder, }, }, webhooks: { "/burgers": { post: createBurgerWebhook, }, }, components: { schemas: { burgerSchema, BurgerIdSchema, orderSchema, OrderIdSchema, }, }, // Adding Speakeasy extensions for better SDK generation "x-speakeasy-retries": { strategy: "backoff", backoff: { initialInterval: 500, maxInterval: 60000, maxElapsedTime: 3600000, exponent: 1.5, }, statusCodes: ["5XX"], retryConnectionErrors: true, }, }); console.log(yaml.stringify(document)); ``` Speakeasy will read the `x-speakeasy-*` extensions to configure the SDK. In this example, the `x-speakeasy-retries` extension will configure the SDK to retry failed requests. For more information on the available extensions, see the [extensions guide](/openapi/extensions). ### Generating the OpenAPI document Run the `index.ts` file to generate the OpenAPI document. ```bash filename="Terminal" npx tsx index.ts > openapi.yaml ``` The output will be a YAML file that looks like this: ```yaml openapi: 3.1.0 info: title: Burger Restaurant API description: An API for managing burgers and orders at a restaurant. version: 1.0.0 servers: - url: https://example.com description: The production server. x-speakeasy-retries: strategy: backoff backoff: initialInterval: 500 maxInterval: 60000 maxElapsedTime: 3600000 exponent: 1.5 statusCodes: - 5XX retryConnectionErrors: true paths: /burgers: post: operationId: createBurger summary: Create a new burger description: Creates a new burger in the database. tags: - burgers requestBody: description: The burger to create. content: application/json: schema: $ref: "#/components/schemas/burgerSchema" responses: "201": description: The burger was created successfully. content: application/json: schema: $ref: "#/components/schemas/burgerSchemaOutput" get: operationId: listBurgers summary: List burgers description: Lists all burgers in the database. tags: - burgers responses: "200": description: The burgers were retrieved successfully. content: application/json: schema: type: array items: $ref: "#/components/schemas/burgerSchemaOutput" /burgers/{id}: get: operationId: getBurger summary: Get a burger description: Gets a burger from the database. tags: - burgers parameters: - in: path name: id schema: $ref: "#/components/schemas/BurgerIdSchema" required: true description: The unique identifier of the burger. responses: "200": description: The burger was retrieved successfully. content: application/json: schema: $ref: "#/components/schemas/burgerSchemaOutput" /orders: post: operationId: createOrder summary: Create a new order description: Creates a new order in the database. tags: - orders requestBody: description: The order to create. content: application/json: schema: $ref: "#/components/schemas/orderSchema" responses: "201": description: The order was created successfully. content: application/json: schema: $ref: "#/components/schemas/orderSchemaOutput" /orders/{id}: get: operationId: getOrder summary: Get an order description: Gets an order from the database. tags: - orders parameters: - in: path name: id schema: $ref: "#/components/schemas/OrderIdSchema" required: true description: The unique identifier of the order. responses: "200": description: The order was retrieved successfully. content: application/json: schema: $ref: "#/components/schemas/orderSchemaOutput" webhooks: /burgers: post: operationId: createBurgerWebhook summary: New burger webhook description: A webhook that is called when a new burger is created. tags: - burgers requestBody: description: The burger that was created. content: application/json: schema: $ref: "#/components/schemas/burgerSchema" responses: "200": description: The webhook was processed successfully. components: schemas: burgerSchema: description: A burger served at the restaurant. type: object properties: id: $ref: "#/components/schemas/BurgerIdSchema" name: description: The name of the burger. example: Veggie Burger type: string minLength: 1 maxLength: 50 description: description: The description of the burger. example: A delicious bean burger with avocado. type: string maxLength: 255 required: - id - name BurgerIdSchema: description: The unique identifier of the burger. example: 1 readOnly: true type: number minimum: 1 orderSchema: description: An order placed at the restaurant. type: object properties: id: $ref: "#/components/schemas/OrderIdSchema" burger_ids: description: The burgers in the order. example: - 1 - 2 minItems: 1 type: array items: $ref: "#/components/schemas/BurgerIdSchema" time: description: The time the order was placed. example: 2021-01-01T00:00:00.000Z type: string format: date-time pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z))$ table: description: The table the order is for. example: 1 type: number minimum: 1 status: description: The status of the order. example: pending type: string enum: - pending - in_progress - ready - delivered note: description: A note for the order. example: No onions. type: string required: - id - burger_ids - time - table - status OrderIdSchema: description: The unique identifier of the order. example: 1 readOnly: true type: number minimum: 1 burgerSchemaOutput: description: A burger served at the restaurant. type: object properties: id: $ref: "#/components/schemas/BurgerIdSchema" name: description: The name of the burger. example: Veggie Burger type: string minLength: 1 maxLength: 50 description: description: The description of the burger. example: A delicious bean burger with avocado. type: string maxLength: 255 required: - id - name additionalProperties: false orderSchemaOutput: description: An order placed at the restaurant. type: object properties: id: $ref: "#/components/schemas/OrderIdSchema" burger_ids: description: The burgers in the order. example: - 1 - 2 minItems: 1 type: array items: $ref: "#/components/schemas/BurgerIdSchema" time: description: The time the order was placed. example: 2021-01-01T00:00:00.000Z type: string format: date-time pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z))$ table: description: The table the order is for. example: 1 type: number minimum: 1 status: description: The status of the order. example: pending type: string enum: - pending - in_progress - ready - delivered note: description: A note for the order. example: No onions. type: string required: - id - burger_ids - time - table - status additionalProperties: false ``` ### Generating an SDK With our OpenAPI document complete, we can now generate an SDK using the Speakeasy SDK generator. #### Installing the Speakeasy CLI First, install the Speakeasy CLI: ```bash filename="Terminal" # Option 1: Using Homebrew (recommended) brew install speakeasy-api/tap/speakeasy # Option 2: Using curl curl -fsSL https://go.speakeasy.com/cli-install.sh | sh ``` #### Linting OpenAPI documents Before generating SDKs, lint the OpenAPI document to catch common issues: ```bash filename="Terminal" speakeasy lint openapi --schema openapi.yaml ``` #### Generating your SDK Now generate your SDK using the quickstart command: ```bash filename="Terminal" speakeasy quickstart ``` Follow the onscreen prompts to provide the necessary configuration details for your new SDK, such as the name, schema location, and output path. Enter `openapi.yaml` when prompted for the OpenAPI document location, and select preferred language when prompted. ## Using your generated SDK Once the SDK is generated, [publish](/docs/publish-sdk) it for use. For TypeScript, it can be published as an NPM package. TypeScript SDKs generated with Speakeasy include an installable [Model Context Protocol (MCP) server](/docs/standalone-mcp/build-server) where the various SDK methods are exposed as tools that AI applications can invoke. The SDK documentation includes instructions for installing the MCP server. <Callout title="Production Readiness" type="warning"> Note that the SDK is not ready for production use immediately after generation. To get it production-ready, follow the steps outlined in the Speakeasy workspace. </Callout> ### Adding SDK generation to your CI/CD pipeline The Speakeasy [`sdk-generation-action`](https://github.com/speakeasy-api/sdk-generation-action) repository provides workflows for integrating the Speakeasy CLI into CI/CD pipelines to automatically regenerate SDKs when your Zod schemas change. Speakeasy can be set up to automatically push a new branch to SDK repositories so that teammates can review and merge the SDK changes. For an overview of how to set up SDK automation, see the Speakeasy [SDK workflow syntax reference](/docs/speakeasy-reference/workflow-file). ## Summary In this tutorial, we learned how to generate OpenAPI schemas from Zod and create client SDKs with Speakeasy. By following these steps, it's possible to ensure an API is well-documented, easy to use, and offers a great developer experience. ### Further reading - [The `zod-openapi` documentation](https://github.com/samchungy/zod-openapi): Learn more about the `zod-openapi` library, including advanced features like custom serializers and middleware integration. - [The Zod documentation](https://zod.dev/): Comprehensive guide to Zod schema validation, including the latest v4 features. # OpenAPI Guides Source: https://speakeasy.com/openapi/guides import { CardGrid } from "@/mdx/components"; import { openapiGeneralGuidesData } from "@/lib/data/docs/openapi-guides"; <CardGrid cards={openapiGeneralGuidesData} heading="" /> # Using Path Fragments to Solve Equivalent Path Signatures Source: https://speakeasy.com/openapi/guides/path-fragments Equivalent path signatures in an OpenAPI specification can cause validation errors and/or unexpected behaviors. Adding unique path fragments is an effective method for resolving these conflicts. Path fragments appended with an anchor (e.g., `#id`, `#name`) make paths unique without altering the API's functionality since the anchor section is not sent in API requests. ## When to Use Path Fragments Path fragments should be used when the OpenAPI specification contains multiple paths with equivalent signatures but distinct parameter names. For example: ```yaml paths: /v13/deployments/{idOrUrl}: get: summary: "Get deployment by ID or URL" parameters: - name: idOrUrl in: path required: true schema: type: string /v13/deployments/{id}: get: summary: "Get deployment by ID" parameters: - name: id in: path required: true schema: type: string ``` These paths conflict because their structures are identical, even though their parameter names differ. ## Add Path Fragments Modify the conflicting paths by appending a unique anchor fragment to each. For example: ```yaml paths: /v13/deployments/{idOrUrl}#idOrUrl: get: summary: "Get deployment by ID or URL" parameters: - name: idOrUrl in: path required: true schema: type: string /v13/deployments/{id}#id: get: summary: "Get deployment by ID" parameters: - name: id in: path required: true schema: type: string ``` Corresponding SDK method signatures: ```typescript // For /v13/deployments/{idOrUrl}#idOrUrl function getDeploymentByIdOrUrl(idOrUrl: string): Deployment { // Implementation } // For /v13/deployments/{id}#id function getDeploymentById(id: string): Deployment { // Implementation } ``` ## Considerations - **Impact on Tooling**: Most tools will handle path fragments correctly, but confirm that all downstream tooling supports the updated specification. - **Path Fragments in Overlays**: Path fragments cannot be added using overlays. They must be introduced directly in the upstream OpenAPI specification. # What is the code samples extension? Source: https://speakeasy.com/openapi/guides/x-codesamples import { Table } from "@/mdx/components"; Many API documentation providers provide code snippets in multiple languages to help developers understand how to use the API. However, these snippets may not correspond to a usage snippet from an existing SDK provided by the API, which reduces the value of the API documentation and can lead to inconsistent integrations, depending on whether a user discovers the API docs or the SDK first. The `x-codeSamples` (previously called `x-code-samples`) extension is a widely accepted spec extension that enables the addition of custom code samples in one or more languages to operation IDs in your OpenAPI specification. When custom code samples are added using the code samples extension, documentation providers will render the usage snippet in the right-hand panel of the documentation page: ![Screenshot of Inkeep's API docs showing featured SDK usage.](/assets/guides/docs-example.png) ## Anatomy of the extension <Table data={[ { fieldName: "lang", type: "string", description: "The language of the code snippet. Can be one from this [list](https://github.com/github-linguist/linguist/blob/master/lib/linguist/popular.yml).", required: "Yes", }, { fieldName: "label", type: "string", description: "Code sample label, for example, `Node` or `Python3`. The `lang` value is used by default.", required: "No", }, { fieldName: "source", type: "string", description: "The code sample source code. In this case, the SDK usage snippet.", required: "Yes", }, ]} columns={[ { key: "fieldName", header: "Field Name" }, { key: "type", header: "Type" }, { key: "description", header: "Description" }, { key: "required", header: "Required" }, ]} /> Documentation providers that support `x-codeSamples` include but are not limited to: - Mintlify - Readme - Redocly - Stoplight ## Example usage Here is a basic example of using the `x-codeSamples` extension with a `curl` snippet. ```yaml openapi: "3.0.3" info: ... tags: [...] paths: /example: get: summary: Example summary description: Example description operationId: examplePath responses: [...] parameters: [...] x-codeSamples: - lang: "cURL" label: "CLI" source: | curl --request POST \ --url 'https://data.apiexample.com/api/example/batch_query/json?format=json' \ --header 'content-type: application/octet-stream: ' \ --data '{}' ``` Now let's extend this to a more complex example: a TypeScript SDK for an LLM chat API. ```yaml openapi: "3.0.3" info: ... tags: [...] paths: /chat_sessions/chat_results: post: summary: Create Chat Session operationId: create tags: [chat_session] requestBody: content: application/json: schema: $ref: "#/components/schemas/CreateChatSession" required: true x-codeSamples: - lang: "typescript" label: "create_chat_session" source: | import { ChatSDK } from "@llm/chat-sdk"; async function run() { const sdk = new ChatSDK({ apiKey: "<API_KEY>", }); const res = await sdk.chatSession.create({ integrationId: "<interagtion_id>", chatSession: { messages: [ { role: "user", content: "How do I get started?", }, ], }, stream: true, }); /* Example of handling a streamed response */ if (res.chatResultStream == null) { throw new Error("failed to create stream: received null value"); } let chatSessionId: string | undefined | null = undefined; for await (const event of res.chatResultStream) { if (event.event == "message_chunk") { console.log("Partial message: " + event.data.contentChunk); chatSessionId = event.data.chatSessionId; } if (event.event == "records_cited") { console.log("Citations: ", JSON.stringify(event.data.citations, null, 2)); } } } run(); ``` Multiple code samples can be added to a single `operationId` to support examples in any number of languages by adding multiple keys under the `x-codeSamples` extension. ```yaml openapi: "3.0.3" info: ... tags: [...] paths: /chat: get: summary: Example summary description: Example description operationId: examplePath responses: [...] parameters: [...] x-codeSamples: - lang: "typescript" label: "chat_ts" source: | ..... ..... - lang: "python" label: "chat_python" source: | ..... ..... ``` ## Generating code samples To generate SDK code samples for your OpenAPI document, run the following command: ```bash speakeasy generate codeSamples -s {{your-spec.yaml}} --langs {{lang1}},{{lang2}} --out code-samples-overlay.yaml ``` This command creates an [overlay](/docs/prep-openapi/overlays/create-overlays) with code samples for every `operationId` in your OpenAPI document. To apply the overlay to your specification, run: ```bash speakeasy overlay apply -o code-samples-overlay.yaml -s {{your-spec.yaml}} -o {{output-spec.yaml}} ``` The final output spec will include `codeSamples` inline. ## Adding code sample generation to your workflow To include `codeSamples` overlay generation in your Speakeasy workflow, add the following to your `.speakeasy/workflow.yaml` for any `target` you have configured: ```yaml filename=".speakeasy/workflow.yaml" targets: my-target: target: typescript source: my-source codeSamples: output: codeSamples.yaml ``` If you want the overlay to be automatically applied on the source, create another workflow entry using `speakeasy configure` as follows: ![Configure Sources 1](/assets/docs/spec-workflow/1.png) Then add the overlay created by Speakeasy to inject code snippets into your spec: ![Configure Sources 2](/assets/docs/spec-workflow/2.png) Finally, provide the name and path for your output OpenAPI spec. This will be the final spec used by Mintlify. ![Configure Sources 3](/assets/docs/spec-workflow/3.png) # Additional installation options coming soon Source: https://speakeasy.com/openapi import { PageHeader, CardGrid } from "@/mdx/components"; import { openapiSections } from "@/lib/data/openapi-sections"; import { allFrameworkGuidesData } from "@/lib/data/openapi/framework-all-guides"; import { TechCards } from "@/components/card/variants/docs/tech-cards"; import { WebGLVideo } from "@/components/webgl/components/video.lazy"; import { GithubIcon } from "@/assets/svg/social/github"; import { openapiGuidesData } from '@/lib/data/docs/openapi-guides'; <div className="relative docs-index bsmnt-container-md flex flex-col mt-10 gap-14"> <WebGLVideo textureUrl="/webgl/bars-logo.mp4" className="absolute top-0 right-0 size-52 md:size-96 max-w-full -mt-10 pointer-events-none" /> <div className="mb-8"> <PageHeader leftAlign eyebrow="Resources" title="OpenAPI Hub" description="The all in one resource for understanding the OpenAPI Specification." className="text-shadow-ascii-contrast" /> </div> <div> ## OpenAPI Basics Start with these foundational concepts to understand and create OpenAPI specifications. <TechCards items={openapiSections?.[0]?.cards} className="my-6" /> </div> <div> ## Advanced Considerations Once you've mastered the basics, explore these advanced topics for more sophisticated API documentation. <CardGrid cards={openapiSections?.[1]?.cards} className="my-6" /> </div> <div> ## OpenAPI Parser Library Work directly with OpenAPI specifications, Arazzo workflows, and OpenAPI Overlays using our comprehensive Go library and CLI tools. <div className="bg-white border border-border dark:border-neutral-800 dark:bg-neutral-900 rounded-[0.75rem] p-6 my-6"> <div className="flex items-start gap-4"> <div className="flex-shrink-0"> <div className="w-10 h-10 bg-neutral-100 dark:bg-neutral-800 rounded-lg flex items-center justify-center"> <GithubIcon className="w-6 h-6 text-muted-foreground" /> </div> </div> <div className="flex-1"> <h3 className="text-lg font-semibold text-gray-900 dark:text-gray-100 mb-2"> <a href="https://github.com/speakeasy-api/openapi" target="_blank" rel="noopener noreferrer" className="interact:text-blue-600 dark:interact:text-blue-400 transition-colors"> speakeasy-api/openapi </a> </h3> <p className="text-gray-600 dark:text-gray-300 mb-4"> Battle-tested Go library for parsing, validating, and manipulating OpenAPI 3.0/3.1 specifications, Arazzo workflow documents, and OpenAPI Overlays. Includes a comprehensive CLI for common operations. </p> <div className="space-y-3"> <div> <h4 className="font-medium text-gray-900 dark:text-gray-100 mb-1">Key features:</h4> <ul className="text-sm text-gray-600 dark:text-gray-300 space-y-1"> <li>• Full OpenAPI 3.0.x and 3.1.x support with validation</li> <li>• Arazzo workflow document parsing and validation</li> <li>• OpenAPI Overlay application and comparison</li> <li>• Document traversal and manipulation APIs</li> <li>• Reference resolution with circular reference handling</li> </ul> </div> <div> <h4 className="font-medium text-gray-900 dark:text-gray-100 mb-1">CLI installation:</h4> ```bash filename="Terminal" go install github.com/speakeasy-api/openapi/cmd/openapi@latest ``` </div> </div> </div> </div> </div> </div> <div> ## Server Framework Guides Generate OpenAPI specs from popular server frameworks: <TechCards className="mt-5" items={allFrameworkGuidesData} /> </div> <div> ## Working with OpenAPI Specs Common guides for working with OpenAPI specs: <CardGrid className="mt-5" cards={openapiGuidesData} heading="" /> </div> </div> # Info Object in OpenAPI Source: https://speakeasy.com/openapi/info import { Table } from "@/mdx/components"; The document's `info` object contains information about the document, including fields like `title`, `version`, and `description` that help to identify the purpose and owner of the document. Example: ```yaml openapi: 3.1.0 info: title: The Speakeasy Bar version: 1.0.0 summary: A bar that serves drinks description: A secret underground bar that serves drinks to those in the know. contact: name: Speakeasy Support url: https://support.speakeasy.bar email: support@speakeasy.bar license: name: Apache 2.0 url: https://www.apache.org/licenses/LICENSE-2.0.html termsOfService: https://speakeasy.bar/terms ``` <Table data={[ { field: "`title`", type: "String", required: "✅", description: "A name for the API contained within the document." }, { field: "`version`", type: "String", required: "✅", description: "The version of this OpenAPI document, _not_ the version of the API or the OpenAPI Specification used. This is recommended to be a [Semantic Version](https://semver.org/)." }, { field: "`summary`", type: "String", required: "", description: "**(Available in OpenAPI 3.1.x ONLY)**<br />A short sentence summarizing the API contained with the document." }, { field: "`description`", type: "String", required: "", description: "A longer description of the API contained within the document. This may contain [CommonMark syntax](https://spec.commonmark.org/) to provide a rich description." }, { field: "`contact`", type: "[Contact Object](#contact-object)", required: "", description: "Contact information for the maintainer of the API.<br /><br /> **Note:** Currently not supported by Speakeasy tooling." }, { field: "`license`", type: "[License Object](#license-object)", required: "", description: "The license the API is made available under." }, { field: "`termsOfService`", type: "String", required: "", description: "A URL to the terms of service for the API." }, { field: "`x-*`", type: "[Extensions](/openapi/extensions)", required: "", description: "Any number of extension fields can be added to the info object that can be used by tooling and vendors to add additional metadata and functionality to the OpenAPI Specification." } ]} columns={[ { key: "field", header: "Field" }, { key: "type", header: "Type" }, { key: "required", header: "Required" }, { key: "description", header: "Description" } ]} /> The above order of fields is recommended (but is not required by the OpenAPI specification) as it puts the most important information first and allows the reader to get a quick overview of the document and API. ## Contact Object in OpenAPI Contact information for the maintainer of the API. <Table data={[ { field: "`name`", type: "String", required: "", description: "The name of a contact that could be approached, for example, for support." }, { field: "`url`", type: "String", required: "", description: "A URL to a website or similar providing contact information." }, { field: "`email`", type: "String", required: "", description: "An email address for the contact." }, { field: "`x-*`", type: "[Extensions](/openapi/extensions)", required: "", description: "Any number of extension fields can be added to the contact object that can be used by tooling and vendors." } ]} columns={[ { key: "field", header: "Field" }, { key: "type", header: "Type" }, { key: "required", header: "Required" }, { key: "description", header: "Description" } ]} /> ## License Object in OpenAPI The license the API is made available under. <Table data={[ { field: "`name`", type: "String", required: "✅", description: "The name of the license." }, { field: "`identifier`", type: "String", required: "", description: "**(Available in OpenAPI 3.1.x ONLY)**<br/>An [SPDX identifier](https://spdx.org/licenses/) for the license. Provided only if `url` isn't set." }, { field: "`url`", type: "String", required: "", description: "A URL to the license information. Provided only if `identifier` isn't set." }, { field: "`x-*`", type: "[Extensions](/openapi/extensions)", required: "", description: "Any number of extension fields can be added to the license object that can be used by tooling and vendors." } ]} columns={[ { key: "field", header: "Field" }, { key: "type", header: "Type" }, { key: "required", header: "Required" }, { key: "description", header: "Description" } ]} /> # The Operation Object in OpenAPI Source: https://speakeasy.com/openapi/operations import { Table } from "@/mdx/components"; An operation object describes a single API operation within a path, including all its possible inputs and outputs and the configuration required to make a successful request. Each operation object corresponds to an HTTP verb, such as `get`, `post`, or `delete`. Example: ```yaml paths: /drinks: get: # The Operation Object operationId: listDrinks summary: Get a list of drinks. description: Get a list of drinks, if authenticated this will include stock levels and product codes otherwise it will only include public information. security: - {} tags: - drinks parameters: - name: type in: query description: The type of drink to filter by. If not provided all drinks will be returned. required: false schema: $ref: "#/components/schemas/DrinkType" responses: "200": description: A list of drinks. content: application/json: schema: type: array items: $ref: "#/components/schemas/Drink" ``` <Table title="Operation Object Fields" data={[ { field: "`operationId`", type: "String", required: "", description: "A unique identifier for the operation, this **_must_** be unique within the document, and is **_case sensitive_**. It is **_recommended_** to always define an `operationId`, but is not required." }, { field: "`deprecated`", type: "Boolean", required: "", description: "Whether the operation is deprecated or not. Defaults to `false`." }, { field: "`summary`", type: "String", required: "", description: "A short summary of what the operation does. This may contain [CommonMark syntax](https://spec.commonmark.org/) to provide a rich description." }, { field: "`description`", type: "String", required: "", description: "A detailed description of the operation, what it does, and how to use it. This may contain [CommonMark syntax](https://spec.commonmark.org/) to provide a rich description." }, { field: "`servers`", type: "[Servers](/openapi/servers)", required: "", description: "A list of [Server Objects](/openapi/servers) that override the servers defined at the document and path levels and apply to this operation." }, { field: "`security`", type: "[Security](/openapi/security)", required: "", description: "A list of [Security Requirement Objects](/openapi/security#security-requirement-object) that override the security requirements defined at the document and path levels and apply to this operation." }, { field: "`x-*`", type: "[Extensions](/openapi/extensions)", required: "", description: "Any number of extension fields can be added to the operation object that can be used by tooling and vendors." }, { field: "`parameters`", type: "[Parameters](/openapi/paths/parameters)", required: "", description: "A list of [Parameter Objects](/openapi/paths/parameters#parameter-object) that are available to this operation. The parameters defined here merge with any defined at the path level, overriding any duplicates." }, { field: "`requestBody`", type: "[Request Body Object](/openapi/paths/operations/requests)", required: "", description: "The request body for this operation where the [HTTP method supports a request body](https://httpwg.org/specs/rfc7231.html). Otherwise, this field is ignored." }, { field: "`responses`", type: "[Responses](/openapi/paths/operations/responses)", required: "✅", description: "A map of [Response Objects](/openapi/paths/operations/responses#response-object) that define the possible responses from executing this operation." }, { field: "`callbacks`", type: "[Callbacks](/openapi/paths/operations/callbacks)", required: "", description: "A map of [Callback Objects](/openapi/paths/operations/callbacks#callback-object) that define possible callbacks that may be executed as a result of this operation." } ]} columns={[ { key: "field", header: "Field" }, { key: "type", header: "Type" }, { key: "required", header: "Required" }, { key: "description", header: "Description" } ]} /> The above order of fields is recommended for defining the fields in the document to help set the stage for the operation and provide a clear understanding of what it does. # OpenAPI Overlays Source: https://speakeasy.com/openapi/overlays import { Callout, Table } from "@/mdx/components"; Overlays allow us to modify an existing OpenAPI document without directly editing the original document. An overlay is a separate document that contains instructions for updating the original OpenAPI document. <Callout title="Active Development" type="warning"> The [OpenAPI Overlay Specification](https://github.com/OAI/Overlay-Specification) has now reached a stable [1.0.0](https://github.com/OAI/Overlay-Specification/releases/tag/1.0.0) release. Speakeasy toolchain utilises a homegrown and OSS implementation. Source code is available [here](https://github.com/speakeasy-api/openapi-overlay). Contributions are welcome! </Callout> Overlays are useful for: - Separating concerns between the original API definition and modifications required by different consumers or use cases. - Avoiding direct modification of the original OpenAPI document, which may be managed by a separate team or process. - Applying a set of common modifications to multiple OpenAPI documents. ## Overlay Document Structure in OpenAPI An Overlay document is a separate document from the OpenAPI document it modifies. It contains an ordered list of [Action Objects](#action-object) that describe the modifications to be made to the original OpenAPI document. ## Overlay Document Structure The following sections describe the structure of an Overlay document: ### `overlay` <Table data={[ { fieldName: "`overlay`", type: "String", required: "✅" } ]} columns={[ { key: "fieldName", header: "Field Name" }, { key: "type", header: "Type" }, { key: "required", header: "Required" } ]} /> The version of the Overlay Specification that the document uses. The value must be a supported [version number](#overlay-specification-versions) ```yaml overlay: 1.0.0 info: title: Overlay to fix the Speakeasy bar version: 0.0.1 actions: - target: "$.tags" description: Add a Snacks tag to the global tags list update: - name: Snacks description: All methods related to serving snacks - target: "$.paths['/dinner']" description: Remove all paths related to serving dinner remove: true ``` ### `info` | Field Name | Type | Required | | ---------- | ----------------------------------- | -------- | | `info` | [Info Object](#overlay-info-object) | ✅ | Provides metadata about the Overlay document. ```yaml overlay: 1.0.0 info: title: Overlay to fix the Speakeasy bar version: 0.0.1 actions: - target: "$.tags" description: Add a Snacks tag to the global tags list update: - name: Snacks description: All methods related to serving snacks - target: "$.paths['/dinner']" description: Remove all paths related to serving dinner remove: true ``` ### `title` | Field Name | Type | Required | | ---------- | ------ | -------- | | `title` | String | ✅ | A human-readable title describing the purpose of the Overlay document. ### `version` <Table data={[ { fieldName: "version", type: "String", required: "✅" } ]} columns={[ { key: "fieldName", header: "Field Name" }, { key: "type", header: "Type" }, { key: "required", header: "Required" } ]} /> A version identifier indicating the version of the Overlay document. ### `actions` | Field Name | Type | Required | | ---------- | --------------------------------- | -------- | | `actions` | [[Action Object](#action-object)] | ✅ | An ordered list of [Action Objects](#action-object) to be applied to the original OpenAPI document. The list must contain at least one [Action Object](#action-object). ### `target` | Field Name | Type | Required | | ---------- | ------ | -------- | | `version` | String | ✅ | A [JSONPath](https://datatracker.ietf.org/wg/jsonpath/documents/) expression that specifies the location in the original OpenAPI document where the change should be made. See [Action Targets](#action-targets). ### `description` | Field Name | Type | Required | | ---------- | ------ | -------- | | `version` | String | | A description of the action. This may contain [CommonMark syntax](https://spec.commonmark.org/) to provide a rich description. ### `update` | Field Name | Type | Required | | ---------- | ------ | -------- | | `version` | String | | An object containing the properties and values to be merged with the objects referenced by the `target`. This field has no effect if the `remove` field is `true`. ### `remove` | Field Name | Type | Required | | ---------- | ------ | -------- | | `version` | String | | If `true`, the objects referenced by the `target` are removed from the original document. If `false` or not provided, the objects are not removed. This field takes precedence over the `update` field. <Table data={[ { fieldName: "overlay", type: "String", required: "✅", description: "The version of the Overlay Specification that the document uses. The value must be a supported [version number](#overlay-specification-versions)." }, { fieldName: "info", type: "[Info Object](#overlay-info-object)", required: "✅", description: "Provides metadata about the Overlay document." }, { fieldName: "extends", type: "String", required: "", description: "A URL to the original OpenAPI document this overlay applies to." }, { fieldName: "actions", type: "[[Action Object](#action-object)]", required: "✅", description: "An ordered list of [Action Objects](#action-object) to be applied to the original OpenAPI document. The list must contain at least one [Action Object](#action-object)." }, { fieldName: "x-*", type: "[Extensions](#extensions)", required: "", description: "Any number of extension fields can be added to the Overlay document that can be used by tooling and vendors. When provided at this level, they apply to the entire Overlay document." } ]} columns={[ { key: "fieldName", header: "Field Name" }, { key: "type", header: "Type" }, { key: "required", header: "Required" }, { key: "description", header: "Description" } ]} /> ## Overlay Specification Versions The `overlay` field contains the version number of the Overlay Specification that the document conforms to. Tooling should use this value to interpret the document correctly. The current version of the Overlay Specification is `1.0.0`, but keep in mind that the specification is still under development. ## Overlay Info Object in OpenAPI Provides metadata about the Overlay document. <Table data={[ { fieldName: "title", type: "String", required: "✅", description: "A human-readable title describing the purpose of the Overlay document." }, { fieldName: "version", type: "String", required: "✅", description: "A version identifier indicating the version of the Overlay document." }, { fieldName: "x-*", type: "Any", required: "", description: "Any number of extension fields can be added that can be used by tooling and vendors." } ]} columns={[ { key: "fieldName", header: "Field Name" }, { key: "type", header: "Type" }, { key: "required", header: "Required" }, { key: "description", header: "Description" } ]} /> ## Action Object in OpenAPI Each Action Object represents at least one change to be made to the original OpenAPI document at the location specified by the `target` field. <Table data={[ { fieldName: "`target`", type: "String", required: "✅", description: "A [JSONPath](https://datatracker.ietf.org/wg/jsonpath/documents/) expression that specifies the location in the original OpenAPI document where the change should be made. See [Action Targets](#action-targets)." }, { fieldName: "`description`", type: "String", required: "", description: "A description of the action. This may contain [CommonMark syntax](https://spec.commonmark.org/) to provide a rich description." }, { fieldName: "`update`", type: "Any", required: "", description: "An object containing the properties and values to be merged with the objects referenced by the `target`. This field has no effect if the `remove` field is `true`." }, { fieldName: "`remove`", type: "Boolean", required: "", description: "If `true`, the objects referenced by the `target` are removed from the original document. If `false` or not provided, the objects are not removed. This field takes precedence over the `update` field." }, { fieldName: "`x-*`", type: "Any", required: "", description: "Any number of extension fields can be added to the Action Object that can be used by tooling and vendors." } ]} columns={[ { key: "fieldName", header: "Field Name" }, { key: "type", header: "Type" }, { key: "required", header: "Required" }, { key: "description", header: "Description" } ]} /> ## Action Targets in OpenAPI The `target` field of an [Action Object](#action-object) is a [JSONPath](https://goessner.net/articles/JsonPath/) expression that specifies the locations in the original OpenAPI document where the change should be made. JSONPath expressions allow you to select and manipulate specific parts of a JSON or YAML document using an intuitive syntax. The expressions are similar to XPath for XML, allowing you to traverse the document tree and select elements based on various criteria. JSONPath is [implemented differently](https://cburgmer.github.io/json-path-comparison/) across tooling languages and among individual tools. Speakeasy uses [VMware Labs YAML JSONPath](https://github.com/vmware-labs/yaml-jsonpath) to parse JSONPath. Here are some examples of JSONPath expressions relevant to OpenAPI documents: <Table data={[ { jsonpathExpression: "`$.info.title`", description: "Selects the `title` field of the `info` object." }, { jsonpathExpression: "`$.servers[0].url`", description: "Selects the `url` field of the first server in the `servers` array." }, { jsonpathExpression: "`$.paths['/drinks'].get.parameters`", description: "Selects the `parameters` of the `get` operation on the `/drinks` path." }, { jsonpathExpression: "`$.paths..parameters[?(@.in=='query')]`", description: "Selects all query parameters across all paths." }, { jsonpathExpression: "`$.paths.*[?(@..parameters.*[?(@.in=='query')])]`", description: "Selects all operations that have one or more query parameters." }, { jsonpathExpression: "`$.paths.*[?(@..parameters.*[?(@.in=='query')])]['post','get','put','path','delete'].tags`", description: "Selects tags of specific operations that have one or more query parameters." }, { jsonpathExpression: "`$.components.schemas.Drink`", description: "Selects the `Drink` schema from the `components.schemas` object." } ]} columns={[ { key: "jsonpathExpression", header: "JSONPath Expression" }, { key: "description", header: "Description" } ]} /> When selecting the object to target for different types of updates, consider the following: <Table data={[ { updateType: "Updating a primitive value (string, number, boolean)", targetObject: "The containing object" }, { updateType: "Updating an object", targetObject: "The object itself" }, { updateType: "Updating an array", targetObject: "The array itself" }, { updateType: "Adding a new property to an object", targetObject: "The object itself" }, { updateType: "Adding a new item to an array", targetObject: "The array itself" }, { updateType: "Removing a property from an object", targetObject: "The object itself" }, { updateType: "Removing an item from an array", targetObject: "The array itself" } ]} columns={[ { key: "updateType", header: "Type of Update" }, { key: "targetObject", header: "Target Object" } ]} /> For example, to update the `description` field of the `info` object, you would target the `info` object itself: ```yaml overlay: 1.0.0 info: title: Update Speakeasy API description version: 1.0.0 actions: - target: $.info update: description: The Speakeasy Bar API is a secret underground bar that serves drinks to those in the know. ``` To remove a specific path, such as `/oldDrinks`, from the `paths` object, you would target that path directly: ```yaml overlay: 1.0.0 info: title: Remove deprecated drinks path version: 1.0.0 actions: - target: $.paths['/oldDrinks'] remove: true ``` ## Applying an Overlay in OpenAPI When an overlay is applied, the `update` object is merged with the targeted objects. Any properties present in both the `update` object and the targeted objects will be replaced with the values from the `update` object. New properties from the `update` object will be added to the targeted objects. The Overlay document is processed in the following order: 1. Tooling locates the original OpenAPI document to modify. This is based on the `extends` field if provided, otherwise determined by the tooling. 2. Each [Action Object](#action-object) is applied to the OpenAPI documents in the order they appear in the `actions` array. For each action: 1. The `target` JSONPath expression is evaluated against the OpenAPI document to locate the objects to modify. 2. If the `remove` field is `true`, the targeted objects are removed from the OpenAPI document. 3. If the `remove` field is `false` or not provided and an `update` object is specified, the `update` object is merged with each of the targeted objects. ## OpenAPI Overlay Examples Here are some examples of overlays that could be applied to the Speakeasy Bar OpenAPI document: ## Updating Info and Servers This example demonstrates updating the `info` and `servers` objects in the original OpenAPI document. ```yaml overlay: 1.0.0 info: title: Update Speakeasy Bar Info and Servers version: 1.0.0 actions: - target: $.info update: description: The Speakeasy Bar API is a secret underground bar that serves drinks to those in the know. contact: name: Speakeasy Bar Support email: support@speakeasy.bar - target: $.servers update: - url: https://staging.speakeasy.bar/v1 description: Staging server - url: https://api.speakeasy.bar/v1 description: Production server ``` ## Adding Tags and Updating Drink Responses This example demonstrates adding tags to the OpenAPI document and updating response objects for operations related to drinks. ```yaml overlay: 1.0.0 info: title: Add Tags and Update Drink Responses version: 1.0.0 actions: - target: $.tags update: - name: Drinks description: Operations related to managing drinks - name: Orders description: Operations related to order processing - target: $.paths['/drinks'].get.responses[200].content['application/json'].schema update: $ref: "#/components/schemas/DrinkList" - target: $.paths['/drinks/{drinkId}'].get.responses[200].content['application/json'].schema update: $ref: "#/components/schemas/Drink" ``` ## Adding Query Parameter Tags This example demonstrates adding a tag to all operations that have query parameters. ```yaml overlay: 1.0.0 info: title: Add Query Parameter Tags version: 1.0.0 actions: - target: $.paths.*[?(@..parameters.*[?(@.in=='query')])]['post','get','put','path','delete'].tags update: - hasQueryParameters ``` ## Removing Deprecated Drink Operations This example demonstrates removing operations related to drinks that have been marked as deprecated. ```yaml overlay: 1.0.0 info: title: Remove Deprecated Drink Operations version: 1.0.0 actions: - target: $.paths['/drinks'].*.deprecated remove: true - target: $.paths['/drinks/{drinkId}'].*.deprecated remove: true ``` <Callout title="Overlay Creation Tool" type="info"> Check out <a href="https://overlay.speakeasy.com/" target="_blank" rel="noopener noreferrer">overlay.speakeasy.com</a> to create and edit your overlays visually. </Callout> <Table data={[ { fieldName: "version", type: "String", required: "" } ]} columns={[ { key: "fieldName", header: "Field Name" }, { key: "type", header: "Type" }, { key: "required", header: "Required" } ]} /> <Table data={[ { fieldName: "actions", type: "[[Action Object](#action-object)]", required: "✅" } ]} columns={[ { key: "fieldName", header: "Field Name" }, { key: "type", header: "Type" }, { key: "required", header: "Required" } ]} /> <Table data={[ { fieldName: "target", type: "String", required: "✅", description: "A [JSONPath](https://datatracker.ietf.org/wg/jsonpath/documents/) expression that specifies the location in the original OpenAPI document where the change should be applied." }, { fieldName: "description", type: "String", required: "", description: "A description of the action. This may contain [CommonMark syntax](https://spec.commonmark.org/) to provide a rich description." }, { fieldName: "update", type: "Any", required: "", description: "An object containing the properties and values to be merged with the objects referenced by the `target`. This field has no effect if the `remove` field is `true`." }, { fieldName: "remove", type: "Boolean", required: "", description: "If `true`, the objects referenced by the `target` are removed from the original document. If `false` or not provided, the objects are not removed. This field takes precedence over the `update` field." }, { fieldName: "x-*", type: "Any", required: "", description: "Any number of extension fields can be added to the Action Object that can be used by tooling and vendors." } ]} columns={[ { key: "fieldName", header: "Field Name" }, { key: "type", header: "Type" }, { key: "required", header: "Required" }, { key: "description", header: "Description" } ]} /> <Table data={[ { jsonpathExpression: "$.info.title", description: "Selects the `title` field of the `info` object." }, { jsonpathExpression: "$.servers[0].url", description: "Selects the `url` field of the first server in the `servers` array." } ]} columns={[ { key: "jsonpathExpression", header: "JSONPath Expression" }, { key: "description", header: "Description" } ]} /> <Table data={[ { fieldName: "`overlay`", type: "String", required: "✅" } ]} columns={[ { key: "fieldName", header: "Field Name" }, { key: "type", header: "Type" }, { key: "required", header: "Required" } ]} /> # Pagination in OpenAPI Source: https://speakeasy.com/openapi/pagination Describing a collection of resources in an API may be simple at first, but certain features like pagination can make it more complex. Pagination is a common requirement for APIs that return large sets of data, which allows for a subsection of a collection to be returned (maybe only 20-100 resources) to avoid overwhelming the server, the client, and all the network components in between. This is usually implemented as a query parameter, such as `?page=1` or `?cursor=abc123`, so that a client can request a specific page of results. The API can then return a subset of the data, and depending on the specific pagination strategy used there could also be metadata about the total number of items and the total number of pages. That metadata allows a client to display pagination controls in the interface such as "Page 1", "Page 2", or simply "Next" and "Previous" buttons. You can learn more about various approaches to pagination in the API design guide [here](/api-design/pagination). This guide will show you how to implement pagination in OpenAPI, regardless of which strategy the API has chosen. ## Pagination with query parameters Query parameters are a common way to implement pagination in APIs. The most common query parameters for pagination are `page` and `limit`, which specify the page number and the number of items per page, respectively. This is a common approach for paginating through large sets of data, and is often used in REST APIs. ```yaml paths: /stations: get: summary: Get a list of train stations description: Returns a paginated and searchable list of all train stations. operationId: get-stations parameters: - name: page in: query description: The page number to return required: false schema: type: integer minimum: 1 default: 1 example: 1 - name: limit in: query description: The number of items to return per page required: false schema: type: integer minimum: 1 maximum: 100 default: 10 example: 10 ``` Adding the `page` and `limit` query parameters advertises to the API consumers that the API supports pagination. Pagination is mentioned in the description too, increasing the chance of it being noticed by any API client developers. The `page` parameter specifies the page number the API should return, and the optional `limit` parameter specifies the number of items to return for that page. This is helpful for mobile apps that want to return a grid of data to the user. For example a 3x3 grid of items, which would require 9 items to be returned, instead of the default 10 giving the user a blank space in the grid. ## Describing pagination metadata When implementing pagination, a common practice is to include metadata in the response to provide information about the total number of items, the current page, and the total number of pages. This metadata can be included in the response body, or sometimes is done with custom HTTP headers (which is frowned upon but done anyway). Using a `meta` object in an array would not work, so APIs often wrap the collection with an "envelope" which might be something like `data`. ```json { "data": [ ... ], "meta": { "page": 2, "size": 10, "total_pages": 100 } ``` If the API is doing this, the response can be described with the following OpenAPI: ```yaml responses: '200': description: A paginated list of train stations. content: application/json: schema: type: object properties: data: type: array items: $ref: '#/components/schemas/Station' meta: type: object properties: page: type: integer example: 2 size: type: integer example: 10 total_pages: type: integer example: 100 ``` This meta object can be defined in `components` and referenced to avoid repeating it in every endpoint. Learn more about components [here](/openapi/components). ## Describing pagination with response links Adding a query parameter to the API is a good start, but asking API clients to construct URLs from little bits of data is always confusing and a recipe for disaster. REST APIs offer the ability to send links which can be used to crawl the API following links like a browser would. If the API supports pagination links they need to be described so clients can use them. The most common way to do this is to include a `links` object in the response. ```json { "data": [ ... ], "links": { "self": "https://api.example.com/stations?page=2", "next": "https://api.example.com/stations?page=3", "prev": "https://api.example.com/stations?page=1" } } ``` That links object can be described in OpenAPI like using the following approach: ```yaml responses: '200': description: OK content: application/json: schema: allOf: - $ref: '#/components/schemas/Wrapper-Collection' - properties: data: type: array items: $ref: '#/components/schemas/Station' - properties: links: allOf: - $ref: '#/components/schemas/Links-Self' - $ref: '#/components/schemas/Links-Pagination' components: Station: description: A train station. type: object properties: id: type: string format: uuid description: Unique identifier for the station. examples: - efdbb9d1-02c2-4bc3-afb7-6788d8782b1e - b2e783e1-c824-4d63-b37a-d8d698862f1d # ... snip ... Links-Self: description: The link to the current resource. type: object properties: self: type: string format: uri Links-Pagination: description: Links to the next and previous pages of a paginated response. type: object properties: next: type: string format: uri prev: type: string format: uri Wrapper-Collection: type: object properties: data: description: The wrapper for a collection is an array of objects. type: array items: type: object links: description: A set of hypermedia links which serve as controls for the client. type: object readOnly: true ``` In this example, the `links` object contains three links: `self`, `next`, and `prev`. The `self` link points to the current page of results, while the `next` and `prev` links point to the next and previous pages of results, respectively. The `links` object is described in the OpenAPI specification using the `links` object, which is a common way to describe hypermedia links in OpenAPI. ## Describing pagination with HTTP headers Some APIs use custom HTTP headers to provide pagination information instead of trying to wedge the information into the response body and using `data` and `meta`. This has the benefit of keeping the JSON clean and tidy, but can be confusing as there is no standard for pagination headers. The `Link` header is a standard HTTP header that can be used to provide links for general HATEOAS purposes but also for pagination specifically, but it does not have anywhere to pass pagination metadata about numbers of pages. For example, an API might use something like `X-Total-Count` header to indicate the total number of items in the collection, and the `X-Page` and `X-Per-Page` headers to indicate the current page and the number of items per page, then next and previous links in the `Link` header. ```yaml paths: /stations: get: summary: Get a list of train stations description: Returns a paginated and searchable list of all train stations. operationId: get-stations responses: '200': description: A paginated list of train stations. headers: X-Total-Count: description: The total number of items in the collection. schema: type: integer example: 1000 X-Page: description: The current page number. schema: type: integer example: 2 X-Per-Page: description: The number of items per page. schema: type: integer example: 10 Links: description: A set of hypermedia links which serve as controls for the client. type: string example: | <https://api.example.com/stations?page=2>; rel="self", <https://api.example.com/stations?page=3>; rel="next", <https://api.example.com/stations?page=1>; rel="prev" ``` However the API is doing pagination, OpenAPI can describe it. The most important thing is to be consistent and clear about how pagination works in the API reference documentation, and if possible write a custom guide for explaining pagination in an API more specifically so that API consumers can get it right. ## Speakeasy SDK pagination Speakeasy SDKs support pagination out of the box, and can be configured to automatically handle pagination for any API, allowing clients to focus on working with data instead of learning about specific pagination strategies. To configure pagination, add the `x-speakeasy-pagination` extension to the OpenAPI description: ```yaml /stations: get: parameters: - name: page in: query schema: type: integer required: true responses: "200": description: OK content: application/json: schema: type: object properties: data: type: array items: type: integer required: - data x-speakeasy-pagination: type: offsetLimit inputs: - name: page in: parameters type: page outputs: results: $.data ``` The `x-speakeasy-pagination` configuration supports `offsetLimit`, `cursor`, and `url` implementations of pagination, and allows the generated SDKs to extract the proper response data from the API instead of having to split up data and metadata manually. Learn more about pagination with Speakeasy SDKs [here](/docs/customize/runtime/pagination). # Paths Object in OpenAPI Source: https://speakeasy.com/openapi/paths import { Table } from "@/mdx/components"; The `paths` object is a map of [Path Item Objects](/openapi/paths#path-item-object) that describes the available paths and operations for the API. Each path is a relative path to the servers defined in the [Servers](/openapi/servers) object, either at the document, path, or operation level. For example, if a server is defined as `https://speakeasy.bar/api` and a path is defined as `/drinks`, the full URL to the path would be `https://speakeasy.bar/api/drinks`, where the path is appended to the server URL. Example: ```yaml paths: /drinks: get: ... # operation definition /drink: get: ... # operation definition put: ... # operation definition post: ... # operation definition delete: ... # operation definition ``` <Table data={[ { field: "`/{path}`", type: "[Path Item Object](/openapi/paths#path-item-object)", required: "", description: "A relative path to an individual endpoint, where the path **_must_** begin with a `/`." }, { field: "`x-*`", type: "[Extensions](/openapi/extensions)", required: "", description: "Any number of extension fields can be added to the paths object that can be used by tooling and vendors." } ]} columns={[ { key: "field", header: "Field" }, { key: "type", header: "Type" }, { key: "required", header: "Required" }, { key: "description", header: "Description" } ]} /> ## Path Item Object in OpenAPI A Path Item Object describes the operations available on a single path. This is generally a map of HTTP methods to [Operation Objects](/openapi/paths/operations) that describe the operations available. It is possible to override the [Servers](/openapi/servers) defined at the document level for a specific path by providing a list of [Server Objects](/openapi/servers) at the path level. It is also possible to provide a list of [Parameters](/openapi/paths/parameters) that are common to all operations defined on the path. Example: ```yaml paths: /drinks: summary: Various operations for browsing and searching drinks description: servers: # Override the servers defined at the document level and apply to all operations defined on this path - url: https://drinks.speakeasy.bar description: The drinks server parameters: # Define a list of parameters that are common to all operations defined on this path - name: type in: query schema: type: string enum: - cocktail - mocktail - spirit - beer - wine - cider get: ... # operation definition ``` Or: ```yaml paths: /drinks: $ref: "#/components/pathItems/drinks" # Reference a Path Item Object defined in the Components Object allowing for reuse in different paths components: pathItems: drinks: servers: - url: https://drinks.speakeasy.bar description: The drinks server parameters: - name: type in: query schema: type: string enum: - cocktail - mocktail - spirit - beer - wine - cider get: ... # operation definition ``` <Table data={[ { field: "`$ref`", type: "String", required: "", description: "Allows for referencing a [Path Item Object](/openapi/paths#path-item-object) defined in the [Components Object](/openapi/components) under the `pathItems` field. If used, no other fields should be set." }, { field: "`summary`", type: "String", required: "", description: "A short summary of what the path item represents. This may contain [CommonMark syntax](https://spec.commonmark.org/) to provide a rich description." }, { field: "`description`", type: "String", required: "", description: "A description of the path item. This may contain [CommonMark syntax](https://spec.commonmark.org/) to provide a rich description." }, { field: "`servers`", type: "[Servers](/openapi/servers)", required: "", description: "A list of [Server Objects](/openapi/servers) that override the servers defined at the document level. Applies to all operations defined on this path." }, { field: "`parameters`", type: "[Parameters](/openapi/paths/parameters)", required: "", description: "A list of [Parameter Objects](/openapi/paths/parameters#parameter-object) that are common to all operations defined on this path." }, { field: "`get`", type: "[Operation Object](/openapi/paths/operations)", required: "", description: "An operation associated with the [`GET` HTTP method](https://developer.mozilla.org/en-US/docs/Web/HTTP/Methods/GET)." }, { field: "`put`", type: "[Operation Object](/openapi/paths/operations)", required: "", description: "An operation associated with the [`PUT` HTTP method](https://developer.mozilla.org/en-US/docs/Web/HTTP/Methods/PUT)." }, { field: "`post`", type: "[Operation Object](/openapi/paths/operations)", required: "", description: "An operation associated with the [`POST` HTTP method](https://developer.mozilla.org/en-US/docs/Web/HTTP/Methods/POST)." }, { field: "`delete`", type: "[Operation Object](/openapi/paths/operations)", required: "", description: "An operation associated with the [`DELETE` HTTP method](https://developer.mozilla.org/en-US/docs/Web/HTTP/Methods/DELETE)." }, { field: "`options`", type: "[Operation Object](/openapi/paths/operations)", required: "", description: "An operation associated with the [`OPTIONS` HTTP method](https://developer.mozilla.org/en-US/docs/Web/HTTP/Methods/OPTIONS)." }, { field: "`head`", type: "[Operation Object](/openapi/paths/operations)", required: "", description: "An operation associated with the [`HEAD` HTTP method](https://developer.mozilla.org/en-US/docs/Web/HTTP/Methods/HEAD)." }, { field: "`patch`", type: "[Operation Object](/openapi/paths/operations)", required: "", description: "An operation associated with the [`PATCH` HTTP method](https://developer.mozilla.org/en-US/docs/Web/HTTP/Methods/PATCH)." }, { field: "`trace`", type: "[Operation Object](/openapi/paths/operations)", required: "", description: "An operation associated with the [`TRACE` HTTP method](https://developer.mozilla.org/en-US/docs/Web/HTTP/Methods/TRACE)." }, { field: "`x-*`", type: "[Extensions](/openapi/extensions)", required: "", description: "Any number of extension fields can be added to the Path Item Object that can be used by tooling and vendors." } ]} columns={[ { key: "field", header: "Field" }, { key: "type", header: "Type" }, { key: "required", header: "Required" }, { key: "description", header: "Description" } ]} /> The order of fields above is recommended but is not significant to the order in which the endpoints should be used. # References ($ref) in OpenAPI Source: https://speakeasy.com/openapi/references import { Table } from "@/mdx/components"; While creating an OpenAPI schema, you might notice duplicated parts in your document. Using references in OpenAPI helps you define a schema once and reuse it elsewhere in the document. This approach minimizes duplication and makes your OpenAPI document more readable and maintainable. To reference a schema, use the `$ref` keyword followed by the path to the schema. The path can be an absolute or relative URI and can also refer to objects in different files. ## OpenAPI Reference Object Any object supported by the [Components Object](/openapi/components) can be replaced by an OpenAPI Reference Object. A Reference Object points to a component using the `$ref` field, which is itself a [JSON Schema Reference](#json-schema-references) and can optionally override the `summary` or `description` of the referenced object. <Table data={[ { field: "`$ref`", type: "String", required: "✅", description: "A [JSON Schema reference](#json-schema-references) to a component." }, { field: "`summary`", type: "String", required: "", description: "A summary that overrides the referenced component's `summary` field. This field is ignored if the referenced component's type does not support the `summary` field." }, { field: "`description`", type: "String", required: "", description: "A detailed description that overrides the referenced component's `description` field. This field is ignored if the referenced component's type does not support the `description` field. This may contain [CommonMark syntax](https://spec.commonmark.org/) to provide a rich description." } ]} columns={[ { key: "field", header: "Field" }, { key: "type", header: "Type" }, { key: "required", header: "Required" }, { key: "description", header: "Description" } ]} /> In the example below, we define a `Drink` schema in the `components` section: ```yaml filename="openapi.yaml" mark=3:11 components: schemas: Drink: type: object summary: A drink in the bar description: A drink that can be ordered in the bar properties: name: type: string recipe: type: string ``` We can reference this component in API paths using a Reference Object: ```yaml filename="openapi.yaml" mark=10:11 paths: /drinks: post: summary: Create a new drink requestBody: required: true content: application/json: schema: $ref: "#/components/schemas/Drink" # References the Drink schema summary: A drink to add to the bar # Overrides the Drink schema summary responses: "200": description: OK ``` In this example, the `Drink` schema is referenced in the `requestBody` of the `POST /drinks` operation. The `summary` field in the Reference Object overrides the `summary` field of the `Drink` schema. ## JSON Schema References in OpenAPI OpenAPI inherits the flexible JSON Schema `$ref` keyword. A JSON Schema reference is an absolute or relative URI that points to a property in the current schema or an external schema. Relative references are resolved using the current document's location as the base URI. Paths inside `$ref` use JSON Pointer syntax (JSON Pointer is different from JSONPath, which is not part of JSON Schema). The JSON Schema `$ref` can reference elements within the same schema or external schemas, or the path defined inside an object's `$id` field. By contrast, OpenAPI Reference Objects are focused on referencing components defined within the `components` section of an OpenAPI document and allow for overriding the `summary` and `description` metadata of the referenced component. The `$id` field in JSON Schema provides a unique identifier for a schema that `$ref` can reference. The `$id` field must be a URI. Most objects in a schema are themselves valid schemas and can thus have an `$id` field. Note that `$id`, not `id`, is a keyword in JSON schema and should be used for references. ## JSON Schema Reference Escape Characters The `/` character separates segments in a JSON Pointer, for instance, `$ref: "#/components/schemas/Drink"`. To refer to a property that contains the `/` character in its name, escape `/` in the `$ref` using `~1`. Since `~` is the escape character in paths, `~` must be escaped as `~0`. For example, consider the JSON object below: ```json filename="example.json" {"a/b": { "c~d": "value" }} ``` To create a pointer to `value`, you would need to use the string `/a~1b/c~0d`. ## Types of References in OpenAPI References in OpenAPI can be relative or absolute. Relative references point to elements within the same API description, while absolute references point to elements in external documents. Absolute references can also point to external resources like online JSON files. Runtime expressions are another type of reference that allows for dynamic values during API execution. <Table data={[ { refString: "`#/components/schemas/Drink`", description: "References the `Drink` schema in the `components` section of the current file." }, { refString: "`./person.yaml`", description: "References the entire `person.yaml` file. The entire content of `person.yaml` is used." }, { refString: "`../person.yaml`", description: "References the `person.yaml` file in the parent directory. The entire content of `person.yaml` is used." }, { refString: "`./people.yaml#/Person`", description: "References the `Person` schema in the `people.yaml` file. Only the `Person` schema from `people.yaml` is used." }, { refString: "`https://pastebin.com/raw/LAvtwJn6`", description: "References an external schema stored online. The entire content of the external schema is used." }, { refString: "`$request.path.orderId`", description: "A runtime expression that passes the `orderId` from the parent operation." } ]} columns={[ { key: "refString", header: "`$ref` string" }, { key: "description", header: "Description" } ]} /> ### Relative References in OpenAPI Relative references specify a location based on the current document and are useful for referencing elements within the same API description. In the example below, the reference points to the `Drink` schema defined within the `components` section of the current OpenAPI document: ```yaml filename="openapi.yaml" mark=9 paths: /order: post: summary: Place an order for a drink requestBody: content: application/json: schema: $ref: "#/components/schemas/Drink" # Relative reference to the Drink schema ``` ### Absolute References in OpenAPI Absolute references include a protocol like `http://` or `https://` followed by the rest of the URI. The example below references an `Ingredient` component in a remote OpenAPI document: ```yaml filename="openapi.yaml" mark=13:14 paths: /drinks: get: summary: Get ingredients responses: "200": description: OK content: application/json: schema: type: array items: # Absolute reference to an external schema $ref: "https://speakeasy.bar/schemas/ingredients.yaml#/components/schemas/Ingredient" ``` ### Runtime Expressions in OpenAPI Runtime expressions allow for dynamically determining values during API execution. These expressions add flexibility and reduce the need for hard coding details in an API description. Expressions in OpenAPI always begin with the dollar sign `$` and indicate the string that follows must be calculated from the HTTP request or response. To embed an expression in another string, wrap it in `{}`. Runtime expressions are commonly used in [Link Objects](/openapi/responses/links) and [Callbacks Objects](/openapi/webhooks/callbacks#callback-object-in-openapi) to pass dynamic values to linked operations or callbacks. An example is: ```yaml filename="openapi.yaml" mark=9 paths: /orders/{orderId}: get: # ... links: viewItems: operationId: getOrderItems parameters: orderId: $request.path.orderId # Pass orderId from the parent operation ``` ## Additional Syntax The following sections show some more advanced ways of using references to structure an API neatly. As the basis of the examples to follow, the following `openapi.yaml` describes a single operation that takes a person's ID and returns their name: ```yaml filename="openapi.yaml" mark=5:25 openapi: 3.1.0 info: title: Person API version: 1.0.0 paths: /persons/{id}: get: parameters: - name: id in: path required: true schema: type: string responses: '200': description: Successful response content: application/json: schema: type: object properties: id: type: string name: type: string ``` ### Local File References The definition of the type that is returned can be moved into its own file so that other operations or other files can use it, too. The operation's `responses` object in `openapi.yaml` now has a `$ref` field: ```yaml filename="openapi.yaml" mark=7 responses: '200': description: Successful response content: application/json: schema: $ref: 'person.yaml' # Reference to the person schema in a separate file ``` In this example, the `$ref` uses a relative path that points to the entire `person.yaml` file in the same folder with the following content: ```yaml filename="person.yaml" type: object properties: id: type: string name: type: string ``` ### Online File References in OpenAPI Schemas can be stored online, for example, in [Pastebin](https://pastebin.com). The reference in `openapi.yaml` can refer to the online `Person` definition: ```yaml filename="openapi.yaml" mark=7 responses: '200': description: Successful response content: application/json: schema: $ref: "https://pastebin.com/raw/LAvtwJn6" ``` The content of the `Person` schema is stored in the Pastebin link `https://pastebin.com/raw/LAvtwJn6`: ```yaml filename="https://pastebin.com/raw/LAvtwJn6" type: object properties: id: type: string name: type: string ``` ### Organize Schemas and Components in Files in OpenAPI While it is common practice to define an OpenAPI document's schemas and components in a single file, a schema _may_ be split across multiple files using references. In a JSON schema file containing multiple objects, the `Person` object might look like this: ```yaml filename="people.yaml" mark=1:7 Person: type: object properties: id: type: string name: type: string Employee: ... Student: ... ``` The `openapi.yaml` OpenAPI document can reference the `Person` schema from `people.yaml` using the filename and path: ```yaml filename="openapi.yaml" mark=7 responses: '200': description: Successful response content: application/json: schema: $ref: "people.yaml#/Person" ``` ### Nested References in OpenAPI References can be nested so that a schema can reference another schema that references a third schema. In the example below, the `Person` schema references the `Address` schema, which in turn references the `Country` schema: ```yaml filename="openapi.yaml" mark=11,20 components: schemas: Person: type: object properties: id: type: string name: type: string address: $ref: "#/components/schemas/Address" Address: type: object properties: street: type: string city: type: string country: $ref: "#/components/schemas/Country" Country: type: string ``` ### Circular References in OpenAPI Circular references are valid in OpenAPI and useful to define recursive objects. In the example below, the `Person` component is redefined to have an array of children, with each child a circular reference to `Person`. ```yaml filename="openapi.yaml" mark=10:13 components: schemas: Person: type: object properties: id: type: string name: type: string children: type: array items: $ref: '#/components/schemas/Person' ``` Although tooling may pass a schema as syntactically valid, it could still be logically unusable. For example, code generation tools that generate SDKs or example requests and responses will fail when used on the schema below that has an infinitely recursive reference: ```yaml filename="infinite-recursion.yaml" components: schemas: Person: $ref: '#/components/schemas/Human' Human: $ref: '#/components/schemas/Person' ``` ### Composition in OpenAPI The `$ref` keyword can be used to compose schemas of multiple objects. Using composition in schemas is described in [`allOf`, `anyOf`, and `oneOf`](/openapi/schemas/objects/polymorphism). The `$ref` keywords used in the examples in that explanation could be external file references instead of component references. ### Arrays and Objects in OpenAPI The `$ref` keyword can be used to replace an entire array or individual items in the array. However, some fields in an OpenAPI schema require each array item or object property to be referenced individually and the entire field may not be replaced with one `$ref`. The fields that must list each item separately are `servers`, `tags`, `paths`, `security`, `securitySchemes/scopes`, and `components`. ## Object Type Examples in OpenAPI This section gives examples for all the places `$ref` can be used. Objects referenced can be in a `components` section of the schema or a separate file. The first example below shows both options, the rest of the examples illustrate referencing objects in the `components` section only. ### Referencing Parameters in OpenAPI The parameters for the operation `listDrinks`: ```yaml filename="openapi.yaml" parameters: - name: type in: query description: The type of drink to filter by. If not provided all drinks will be returned. required: false schema: $ref: "#/components/schemas/DrinkType" ``` The same schema with a reference to the `components` section: ```yaml filename="openapi.yaml" parameters: - $ref: '#/components/parameters/DrinkTypeParameter' # ... components: parameters: DrinkTypeParameter: name: type in: query description: The type of drink to filter by. If not provided all drinks will be returned. required: false schema: $ref: '#/components/schemas/DrinkType' schemas: DrinkType: # ... ``` The schema using an external file reference instead of `components`: ```yaml filename="openapi.yaml" parameters: - $ref: 'parameters.yaml#/DrinkTypeParameter' ``` The contents of the referenced `parameters.yaml` file (note that the main schema file is referenced in turn from the `openapi.yaml` file): ```yaml filename="parameters.yaml" DrinkTypeParameter: name: type in: query description: The type of drink to filter by. If not provided all drinks will be returned. required: false schema: $ref: 'openapi.yaml#/components/schemas/DrinkType' ``` ### Referencing Request Bodies in OpenAPI The `requestBody` for the operation `authenticate`: ```yaml filename="openapi.yaml" requestBody: required: true content: application/json: schema: type: object properties: username: type: string password: type: string ``` The same schema using a reference to the `components` section: ```yaml filename="openapi.yaml" requestBody: $ref: '#/components/requestBodies/UserCredentials' # ... components: requestBodies: UserCredentials: required: true content: application/json: schema: type: object properties: username: type: string password: type: string ``` ### Referencing Error Types in OpenAPI All operations in the Bar schema already use references for error types: ```yaml filename="openapi.yaml" responses: "200": description: The api key to use for authenticated endpoints. content: application/json: schema: type: object properties: token: type: string "401": description: Invalid credentials provided. "5XX": $ref: "#/components/responses/APIError" default: $ref: "#/components/responses/UnknownError" ... components: responses: APIError: description: An error occurred interacting with the API. content: application/json: schema: $ref: "#/components/schemas/APIError" UnknownError: description: An unknown error occurred interacting with the API. content: application/json: schema: $ref: "#/components/schemas/Error" ``` ### Referencing Security Schemes in OpenAPI Security schemes automatically reference `components` and do not need `$ref`: ```yaml filename="openapi.yaml" paths /drinks: post: operationId: createDrink summary: Create a drink. description: Create a drink. Only available when authenticated. security: - apiKey: [] ... components: securitySchemes: apiKey: type: apiKey name: Authorization in: header ``` Referencing schemes in a separate file: ```yaml components: securitySchemes: $ref: 'securitySchemes.yaml' ``` ### Referencing Schema Examples in OpenAPI In the `responses` section of the `listDrinks` operation, `examples` use `$ref`: ```yaml responses: "200": description: A list of drinks. content: application/json: schema: type: array items: oneOf: - $ref: "#/components/schemas/Drink" - $ref: "#/components/schemas/PublicDrink" discriminator: propertyName: dataLevel mapping: unauthenticated: "#/components/schemas/PublicDrink" authenticated: "#/components/schemas/Drink" examples: unauthenticated_drinks: $ref: "#/components/examples/unauthenticated_drinks" ... components: examples: unauthenticated_drinks: summary: A list of drinks for unauthenticated users value: [ { "name": "Old Fashioned", "type": "cocktail", "price": 1000, "photo": "https://speakeasy.bar/drinks/old_fashioned.jpg", "dataLevel": "unauthenticated", }, ... ] ``` The schemes in a separate file can be referenced like this: ```yaml components: examples: $ref: 'examples.yaml' ``` ### Discriminators in OpenAPI [Discriminators](/openapi/schemas/objects/polymorphism#discriminator-object-in-openapi) can be used to differentiate between different schemas in a `oneOf` array. The `mapping` object in a discriminator maps values to schemas. The references in these mappings are similar to the values used in `$ref`. In the example below, the drinks returned by the `listDrinks` operation can be either `Cocktail` or `Beer`: ```yaml filename="openapi.yaml" mark=14:16 responses: "200": description: A list of drinks. content: application/json: schema: type: array items: oneOf: - $ref: "#/components/schemas/Cocktail" - $ref: "#/components/schemas/Beer" discriminator: propertyName: category mapping: cocktail: "#/components/schemas/Cocktail" beer: "#/components/schemas/Beer" ``` ## References Best Practices in OpenAPI There are no syntactical or functional differences between a schema in one file or split into multiple files using `$ref`. But splitting a large schema into separate files implements the principle of modularity, which holds several advantages for users: - Readability: Multiple shorter files with appropriate names are easier to navigate and understand than one massive file. - Reusability: Multiple top-level schemas that are unrelated can reuse lower-level schemas stored in shared external files. - Collaboration: Programmers can more easily edit smaller files in their area of focus in an API without version control conflicts. A minor disadvantage of using multiple files is increased administration. Deployments need to carefully validate and distribute multiple files with interdependencies instead of just one file. The separate files may be stored in different locations on a network and have complicated URI resolutions, though this is usually unnecessary. Given that splitting a schema into several files is beneficial, let's consider how to do it. Here are some principles to consider: ### Use External Files Sparingly A potential downside of separating your OpenAPI documents into multiple files with references is that online validators can't validate multiple files at once. When using multiple documents, you can validate your OpenAPI schema using local validators. For example: ```sh npx swagger-cli validate openapi.yaml ``` If both files are present and valid, the validator will return `openapi.yaml is valid`. ### Use Components When Necessary Don't waste time on premature optimization or modularization. If you're using a type only once, don't bother moving it into components. But as soon as you use a type twice, use two `$ref` keywords in your main schema, and move the type definition down into `components`. Now if you want to split your file into multiple files, your types are already modules. When should you break your `components` section into multiple files? Either when the file becomes too large to be easily readable, or when you start writing another API that reuses the same components as your original one. ### Design Versioning Carefully It is important to specify the version number of your schemas so that customers can be certain they are calling the correct API for the code they have written. As well as having a version number in your YAML, you should also number your schema filenames or use Git release numbers: ```txt ├── schema-v1.yaml ├── schema-v2.yaml ``` When you split your schema into multiple files, it's easier to do versioning at the folder level: ```txt ├── api ├── v1 | ├── openapi.yaml | └── person.yaml └── v2 ├── openapi.yaml └── person.yaml ``` Even if only `openapi.yaml` changes when releasing a new version, and `person.yaml` remains the same, it is simpler to copy all files into the new version folder rather than referencing the old `person.yaml` from both `openapi.yaml` files. There is too much risk of making a change in one version of a file that breaks other files depending on it. If multiple APIs share common components, versioning becomes more complex. Consider the example below. ```txt ├── api | ├── v1 | | ├── bar-schema.yaml | | └── employee-schema.yaml | └── v2 | ├── bar-schema.yaml | └── employee-schema.yaml | └── shared ├── v1 | └── person.yaml └── v2 └── person.yaml ``` Two APIs, Bar and Employee, use the person schema kept in a shared folder. When version 1 of the person schema is updated for the Bar API, you need to either: - The Employee API must be updated and a new version released. Since there have been no functional changes to the API, there is no benefit to customers updating their code that uses the API. This is a poor solution. - The file should no longer be shared. Different versions of the `person.yaml` file should be moved into the Bar and Employee folders and deleted from the `shared` folder. This solution discards the modularity and reusability of shared files. - The Bar API version 2 should point to the person schema version 2, and the Employee API version 1 should point to the person schema version 1. The final solution makes the most sense, but is dangerous. You'll need to keep track of which versions of the APIs point to which version of the shared schema, and further changes to `person.yaml` version 1 could cause it to diverge from version 2, and you'll need to implement one of the three solutions above again. Note that `$ref` has no version checking. You need to create your own scripts to validate the versioning system and folder structure your company decides to use. ### How To Structure Your Files In general, choose filenames that match the file contents, such as `parameters.yaml`, `responses.yaml`, or `securitySchemes.yaml`. Group each type of object into a single file, such as `schemas`, `examples`, and so on. A simple folder structure might look like this: ```txt /api openapi.yaml /components schemas.yaml responses.yaml parameters.yaml examples.yaml security.yaml /paths users.yaml products.yaml ``` When referencing external files, use clear and relative paths that make it easy to understand where the referenced file is located relative to the current file. Don't chain references more than two levels. Deep nesting is difficult to understand. Each file should be alphabetically structured to make finding elements easy. # OpenAPI release notes Source: https://speakeasy.com/openapi/release-notes import { Callout } from "@/mdx/components"; Each release of the OpenAPI Specification introduces new features, schema refinements, and structural changes - some safe, others breaking - that influence how developers design, describe, and document their APIs. <Callout title="Note" variant="info"> This page is not an official OpenAPI release notes page. It explains key version changes with context, examples, and practical insights to help you decide whether to upgrade. </Callout> OpenAPI 3.2.0 (September 2025) is the newest feature release. It adds structured tag navigation, streaming-friendly media types, and fresh OAuth flows onto the JSON Schema-aligned foundation introduced in 3.1. Refer to the [official release notes](https://github.com/OAI/OpenAPI-Specification/releases/tag/3.2.0) for the canonical changelog. ## OpenAPI version history | **Version** | **Release date** | **Changes** | **Breaking changes** | |-------------------|------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|----------------------| | **OpenAPI 3.2.0** | Sep 19, 2025 | - Adds hierarchical tag metadata (`summary`, `parent`, `kind`) fields and reusable media types<br />- Supports streaming-friendly payloads with `itemSchema`, `query` HTTP operations, and `querystring` parameters<br />- Expands security (OAuth2 device flow, metadata URL), responses (`summary`), and server naming options | Limited | | **OpenAPI 3.1.1** | Oct 24, 2024 | - Clarifies required fields and schema interpretation<br />- Improves JSON Schema vocabulary integration<br />- Refines OAuth 2.0 and bearer token documentation<br />- Fixes minor spec language and formatting<br />- Confirms no breaking changes from 3.1.0 | No | | **OpenAPI 3.1.0** | Feb 16, 2021 | - Achieves full JSON Schema 2020-12 compliance<br />- Adds support for the `webhooks` object<br />- Allows `$schema` declaration at the document root<br />- Makes `example` and `examples` mutually exclusive<br />- Removes the SemVer requirement in `info.version`<br />- Deprecates `nullable` in favor of `type: [T, "null"]`<br />- Relaxes rules around the `openapi` field | Yes | | **OpenAPI 3.0.0** | July 26, 2017 | - Overhauls the request body structure (`content` replaces `formData`, `body`)<br />- Introduces `components` for reusable schemas, parameters, and responses<br />- Improves parameter serialization and content negotiation<br />- Adds `callbacks` for async APIs<br />- Enhances documentation via `tags`, `externalDocs`, and `examples` | Yes | ## OpenAPI 3.2 vs 3.1 OpenAPI 3.2.0 builds on the JSON Schema 2020-12 alignment introduced in 3.1.0. It is a feature release: Your existing 3.1 descriptions keep working while you opt into new ergonomics for documentation, streaming, and security. The update is particularly useful if you publish Server-Sent Events (SSE) feeds or Model Context Protocol (MCP) connectors and want everything captured in one OpenAPI file. ### At a glance - **Tag metadata is now standardized** with `summary`, `parent`, and `kind`, replacing the vendor extensions many teams relied on. - **Streaming payloads gain first-class support** through `itemSchema`, `prefixEncoding`, and guidance for sequential media types, such as SSE, JSON Lines, and multipart feeds. - **Security schemes pick up OAuth 2.0 device authorization**, metadata URLs, a `deprecated` flag, and the option to reference shared schemes by URI. - **Documentation polish** includes response `summary`, example `dataValue`/`serializedValue`, reusable `components.mediaTypes`, server `name`, and the top-level `$self` field. #### Quick comparison | **Capability** | **OpenAPI 3.1** | **OpenAPI 3.2** | |--------------------------|----------------------------------------------------------------------------|-----------------------------------------------------------------------------------------------------------------------------------| | **Tag navigation** | Relies on `x-displayName` / `x-tagGroups` vendor extensions | Native `summary`, `parent`, and `kind` fields with registry-backed conventions | | **Streaming payloads** | SSE and sequential feeds require callbacks or prose workarounds | `itemSchema`, `prefixEncoding`, `itemEncoding`, and sequential media types recognized | | **HTTP operations** | Standard verbs only | Adds `query` and `additionalOperations` for uncommon verbs | | **Parameters** | `query`, `header`, `path`, `cookie` locations | Adds `querystring`, extends `allowReserved`, and introduces a cookie `style` | | **Security metadata** | OAuth flows without device support or metadata URLs | Device flow, `oauth2MetadataUrl`, `deprecated`, and URI-based references | | **Documentation polish** | Response `description` required, examples lack structured/serialized split | Optional response `description`, new `summary` field, example `dataValue`/`serializedValue` fields, server `name`, `$self` fields | ### What's new when you move to 3.2? OpenAPI 3.2 keeps the OpenAPI 3.1 descriptions valid while unlocking several headline upgrades. Here's what changes for teams planning the move: - **Navigation metadata:** Standard tag fields (`summary`, `parent`, and `kind`) replace the common `x-displayName` / `x-tagGroups` pattern. You can keep existing vendor extensions for backwards compatibility, but update doc toolchains so they favor the official fields. - **Streaming support:** The addition of `itemSchema`, `prefixEncoding`, and sequential media type guidance makes SSE, JSON Lines (JSONL), and MCP channels first-class citizens. Preview how your generators or SDKs render stream payloads before switching production specs. - **HTTP coverage:** The new `query` operation, `additionalOperations`, and `querystring` parameter location align the spec with real-world verbs and encodings. Check linters and gateways for compatibility — old keywords still work, but new ones may require upgrades. - **Docs and security polish:** Addition of response `summary` field, example `dataValue`/`serializedValue` fields, reusable `components.mediaTypes`, server `name`, `$self` fields, OAuth device flows, metadata URLs, and URI-referenced schemes all streamline governance. Adopt them incrementally; every feature is opt-in. Ready for the deep dive? Head to [What's new in OpenAPI 3.2?](#whats-new-in-openapi-32) for implementation examples and notes. ### Should you upgrade? **Upgrade now if:** - Your docs currently rely on `x-displayName` / `x-tagGroups` or you want cleaner nested navigation - You expose streaming APIs (SSE, JSON Lines, multipart feeds) or MCP connectors and want generators to understand them - Security reviews would benefit from the OAuth device flow, metadata URLs, or scheme deprecation signals - You plan to publish reusable media type definitions or need a canonical `$self` URL for multi-document specs **Hold off if:** - Your tooling pipeline cannot yet parse `query` operations, the `querystring` parameter location, or the new example fields - You are mid-migration from OpenAPI 2.0 or 3.0 and prefer to stabilize on 3.1 before adopting new keywords - Third-party consumers depend on current vendor extensions and need time to add support for the standardized fields ## OpenAPI 3.2 vs 3.0 If you're still on OpenAPI 3.0, moving straight to 3.2 lets you skip an intermediate upgrade while adopting both the OpenAPI 3.1 JSON Schema alignment and the OpenAPI 3.2 usability improvements. OpenAPI 3.1.0 was a mostly iterative release, with some breaking changes, whereas 3.2.0 is a feature release. ## OpenAPI 3.1 vs 3.0 OpenAPI 3.1 introduces JSON Schema compliance, improved webhooks support, and breaking changes like the discontinued support for semantic versioning. You may want to stick to OpenAPI 3.0 because it offers: - **Easier migration from OpenAPI 2.0:** Incremental changes make the transition smoother. - **More stable tooling support:** Many API tools and frameworks still default to OpenAPI 3.0. ## OpenAPI 3.1.1 vs 3.1.0 [OpenAPI 3.1.1](https://github.com/OAI/OpenAPI-Specification/releases/tag/3.1.1) is not a feature release but a refinement of OpenAPI 3.1.0. It improves terminology, JSON Schema alignment, reference handling, and documentation clarity without introducing breaking changes from OpenAPI 3.1.0. The most interesting change is in terminology. Previously, "OpenAPI document" and "OpenAPI definition" were used inconsistently. OpenAPI 3.1.1 standardizes these definitions: - **OpenAPI description:** The complete API definition, which may span multiple documents. - **OpenAPI document:** A single file that makes up the whole or a part of an OpenAPI description. - **OpenAPI entry document:** The starting point of an OpenAPI description, from which references can be resolved. If your OpenAPI definition spans multiple files, the entry document is the file that tools like Swagger or Redoc first load to process your API definition. ## What's new in OpenAPI 3.2? OpenAPI 3.2.0 folds years of community extensions directly into the specification. Use this deep dive when you're ready to implement the changes. The sections below walk through the navigation metadata, streaming semantics, and security tooling, including snippets and migration tips. ### Hierarchical tags and navigation metadata Tag objects now ship with the `summary`, `parent`, and `kind` fields, so documentation tools can build nested navigation without relying on `x-displayName` or `x-tagGroups`. The optional `kind` field works with the OpenAPI registry to keep category names consistent across docs, and it lets you earmark tags for specific audiences (docs-only, SDK badges, internal tooling) without polluting navigation. As you migrate, consider contributing new `kind` values back to the community registry, so other teams can reuse the same taxonomy. ```yaml tags: - name: accounts summary: Account management kind: nav - name: accounts.reports summary: Analytics reports parent: accounts kind: badge ``` You can mix these standard fields with existing vendor extensions while your tooling catches up. ### SSE, MCP streams, and other streaming media types OpenAPI 3.2 makes Server-Sent Events (SSE) a first-class citizen. Describe each message with `itemSchema`, and tooling will treat the response as a stream rather than an opaque blob. The same technique covers `application/jsonl`, `application/json-seq`, and multipart payloads, and you can adjust chunk boundaries with `prefixEncoding` and `itemEncoding`. If you ship Model Context Protocol (MCP) connectors over SSE, you can now keep the stream contract in the same OpenAPI document as the rest of the API. The spec explicitly recognizes `text/event-stream`, `application/jsonl`, `application/json-seq`, and `multipart/mixed`, which gives generators consistent guidance for chat, AI, IoT, and financial-data feeds that were previously awkward to document. With the stream semantics defined in OpenAPI, MCP server generators can wire up REST setup endpoints and SSE tool channels from one source, instead of juggling separate manifests. ```yaml content: multipart/mixed: prefixEncoding: boundary: live-feed itemSchema: type: object properties: id: type: string attachment: type: string format: binary itemEncoding: attachment: contentType: image/png ``` With `itemSchema` in place, documentation portals can render example SSE payloads line-by-line, and client SDKs can surface typed callbacks, such as `onPriceUpdate`, without additional vendor extensions. You can also pre-register shared SSE schemas under `components.mediaTypes` to keep event definitions consistent across multiple endpoints. #### SSE authoring checklist - Model each SSE field (`event`, `id`, `retry`, and `data`) in the schema, so consumers know which keys to expect. - Document reconnection behavior by pairing the SSE response with `Last-Event-ID` headers or query parameters. - Add an example showing the literal wire format (`serializedValue`) alongside the structured `dataValue` object that will be parsed by the client. - Mention compatible client tooling (for example, browsers' `EventSource` API or server frameworks) in accompanying guides to capture discovery traffic. - If your API powers MCP connectors, note the tool IDs or channel names, so agent builders can map SSE topics to MCP tools without hunting through extra docs. ### HTTP operation and parameter updates OpenAPI 3.2 expands HTTP coverage with a dedicated `query` method and an `additionalOperations` bucket for less common verbs. The `query` verb formalizes safe, idempotent payload-driven lookups, so you no longer have to overload `POST` to support complex search builders. Parameters can target the entire `querystring`, letting you model structured filters as a single Schema Object, `allowReserved` works wherever percent-encoding would normally apply, and cookies gain a purpose-built `cookie` style that preserves delimiters. ```yaml paths: /preferences: query: summary: Fetch preference suggestions parameters: - in: querystring name: payload content: application/json: schema: type: object - in: cookie name: preferences style: cookie explode: false additionalOperations: PROPFIND: summary: Inspect stored preference metadata ``` ### Examples, responses, and documentation polish The Example Object gains `dataValue` and `serializedValue` fields, so you can show both structured data and the literal payload. Responses now support a short `summary` and no longer require `description`, and `components.mediaTypes` lets you register reusable content definitions. A new `name` field for servers, plus the top-level `$self` URI, make multi-environment docs easier to navigate. ```yaml components: mediaTypes: application/jsonl: schema: $ref: '#/components/schemas/ResultEvent' paths: /results: get: responses: "200": summary: Batch of results content: application/jsonl: examples: sample: dataValue: id: evt_01 status: success serializedValue: '{"id":"evt_01","status":"success"}' servers: - url: https://sandbox.api.example.com name: Sandbox $self: https://api.example.com/openapi.yaml ``` ### Security, server, and document governance OAuth 2.0 device flows join the core spec alongside an `oauth2MetadataUrl` for OpenID Connect discovery documents. The device profile is tailored for smart TVs, kiosks, and other limited-input form factors, so you can model the hand-off without falling back to prose. Security schemes can be flagged as `deprecated`, and you can reference shared schemes by URI. The metadata URL support mirrors how open finance ecosystems publish discovery endpoints, and together with formalized URL-resolution guidance and `$self`, federated specs become easier to compose. ```yaml components: securitySchemes: oauthDevice: type: oauth2 oauth2MetadataUrl: https://auth.example.com/.well-known/oauth-authorization-server deprecated: false flows: deviceAuthorization: deviceAuthorizationUrl: https://auth.example.com/device tokenUrl: https://auth.example.com/token scopes: reports: Read usage reports security: - https://security.example.com/schemes/api-key: [] - oauthDevice: [reports] ``` ### XML and polymorphism improvements XML-heavy APIs receive a new `nodeType` property that replaces `attribute: true` and clarifies how arrays and CDATA map to XML nodes. Discriminators now accept a `defaultMapping`, and `propertyName` can be omitted when tooling infers the correct schema. ```yaml components: schemas: Customer: type: object properties: id: type: string xml: nodeType: attribute notes: type: string xml: nodeType: cdata Payment: oneOf: - $ref: '#/components/schemas/CardPayment' - $ref: '#/components/schemas/BankTransfer' discriminator: defaultMapping: '#/components/schemas/CardPayment' ``` ## What's new in OpenAPI 3.1? OpenAPI 3.1 introduces the following changes. ### Full JSON Schema 2019-09 support In OpenAPI 3.0, handling `null` values requires the use of a separate `nullable` property, which is inconsistent with JSON Schema standards. In OpenAPI 3.1, `nullable` has been removed, and you can use `null` directly as a valid type, which aligns with the JSON Schema 2019-09. ### Nullable in OpenAPI 3.0 The following schema demonstrates how to define nullability in OpenAPI 3.0: ```yaml type: object properties: value: type: string nullable: true ``` ### Null in OpenAPI 3.1 The equivalent schema in OpenAPI 3.1 uses a union type to represent `null`: ```yaml type: object properties: value: type: ["string", "null"] ``` You can learn more about nullability in Speakeasy's [OpenAPI schemas](/openapi/schemas/null) documentation. ### Webhooks support Before OpenAPI 3.x, the OpenAPI Specification didn't provide a way to describe event-driven behavior. OpenAPI 3.0 introduces `callbacks`, which allow for the documenting of asynchronous requests from the server back to the client. While `callbacks` aren't a perfect fit for modeling webhooks, they are often used as a workaround for doing so. OpenAPI 3.1 introduces a dedicated `webhooks` field for documenting outgoing API events (for example, notifying a client when a user is created). ### Webhooks in OpenAPI 3.0 In OpenAPI 3.0, you need to use a workaround like `callbacks` to document webhooks: ```yaml paths: /subscribe: post: summary: Subscribe to price change events requestBody: required: true content: application/json: schema: type: object properties: callbackUrl: type: string format: uri required: - callbackUrl responses: "202": description: Subscription accepted callbacks: onPriceChange: "{$request.body#/callbackUrl}": post: summary: Price change notification requestBody: required: true content: application/json: schema: type: object properties: id: type: string price: type: number required: - id - price responses: "200": description: Notification received ``` ### Webhooks in OpenAPI 3.1 With OpenAPI 3.1, you can define `webhooks` directly: ```yaml webhooks: userRegistered: post: requestBody: content: application/json: schema: type: object properties: userId: type: string responses: "200": description: Acknowledged ``` OpenAPI does not provide a way to explicitly link a webhook with its registration, which is needed to allow users to subscribe to alerts, but you can address this by following the Speakeasy guide to [using webhooks in OpenAPI](/post/openapi-tips-webhooks-callbacks#creating-a-webhook-in-openapi). ### Path items in components In OpenAPI 3.0, if you have multiple endpoints with similar structures (for example, user-related paths like `/users/{id}` and `/admins/{id}`), you have to repeat the same request structure for each path. OpenAPI 3.1 allows reusable path item objects to be stored in `components.pathItems`, reducing duplication and improving maintainability. ### Paths in OpenAPI 3.0 In OpenAPI 3.0, each path has to be fully defined, even if multiple endpoints share the same request structure: ```yaml paths: /users/{id}: get: summary: Get user information parameters: - name: id in: path required: true schema: type: string responses: "200": description: User data /admins/{id}: get: summary: Get admin information parameters: - name: id in: path required: true schema: type: string responses: "200": description: Admin data ``` ### Paths in OpenAPI 3.1 In OpenAPI 3.1, you can define the path structure once and reuse it across multiple paths: ```yaml components: pathItems: userLookup: get: summary: Get user or admin information parameters: - name: id in: path required: true schema: type: string responses: "200": description: User or admin data paths: /users/{id}: $ref: "#/components/pathItems/userLookup" /admins/{id}: $ref: "#/components/pathItems/userLookup" ``` ### Breaking changes in OpenAPI 3.1 Apart from these structural improvements, **OpenAPI 3.1 introduces breaking changes** that may require modifications to existing OpenAPI documents. Here are the most important ones: - **OpenAPI no longer follows semantic versioning (SemVer):** Future updates may introduce breaking changes, even in minor version increments. - **`exclusiveMaximum` and `exclusiveMinimum` values must be numeric:** Boolean values (`true` and `false`) are no longer valid. Use a numeric limit instead. - **`format` no longer defines file payloads:** You can no longer use `format: binary` or `format: base64`. Use `contentEncoding` and `contentMediaType` instead. - **The `jsonSchemaDialect` field has been added:** A new top-level field allows you to specify the default `$schema` value for schema objects, ensuring consistency in JSON Schema validation. Visit the OpenAPI GitHub repository to learn more about the changes introduced in [OpenAPI 3.0](https://github.com/OAI/OpenAPI-Specification/releases/tag/3.0.0) and [OpenAPI 3.1](https://github.com/OAI/OpenAPI-Specification/releases/tag/3.1.0). # Request Body Object in OpenAPI Source: https://speakeasy.com/openapi/requests import { Table } from "@/mdx/components"; The request body is used to describe the HTTP body of the request for operations. Not all operations require a request body, but when they do, the request body is defined in the `requestBody` field of the operation object. ```yaml paths: /drinks: post: requestBody: content: application/json: schema: type: object properties: name: type: string description: The name of the drink. ingredients: type: array items: type: string description: The ingredients of the drink. instructions: type: string description: Instructions to prepare the drink. ``` ## Request Body Object Here's how the `requestBody` object is structured: <Table title="Request Body Object Fields" data={[ { field: "`description`", type: "String", required: "", description: "A description of the request body. This may contain [CommonMark syntax](https://spec.commonmark.org/) to provide a rich description." }, { field: "`content`", type: "[Content](/openapi/paths/operations/content)", required: "✅", description: "A map of [Media Type Objects](/openapi/paths/operations/content#media-type-object) that defines the possible media types that can be used for the request body." }, { field: "`required`", type: "Boolean", required: "", description: "Whether the request body is required. Defaults to `false`." }, { field: "`x-*`", type: "[Extensions](/openapi/extensions)", required: "", description: "Any number of extension fields can be added to the Request Body Object that can be used by tooling and vendors." } ]} columns={[ { key: "field", header: "Field" }, { key: "type", header: "Type" }, { key: "required", header: "Required" }, { key: "description", header: "Description" } ]} /> ## Required vs optional Request bodies are optional by default in OpenAPI, so adding the `requestBody` property does not mean it expects the HTTP request to actually be present. It means it _can_ be present. This can be changed by setting the `required` property to `true`, but it is often forgotten and lots of tooling will remind you to add a required property, even if its set to `required: false`, just to make sure there is nothing spooky or unexpected happening. ```yaml paths: /drinks: post: requestBody: description: The drink to create. required: true content: application/json: schema: type: object properties: name: type: string description: The name of the drink. ingredients: type: array items: type: string description: The ingredients of the drink. instructions: type: string description: Instructions to prepare the drink. ``` Getting this right is not just important for API documentation, but for generated SDKs that should know whether or not to throw errors, and data validations libraries/middlewares which should know whether or not to reject an invalid request. When should a request body be optional? Not all that often, but it can be useful for things like `PATCH` requests where no changes are happening but you want to "touch" the resource to update the `updatedAt` timestamp. ```http PATCH /drinks/1 HTTP/1.1 Host: api.example.org ``` To support this use case, the `requestBody` can be set to be optional. ```yaml paths: /drinks/{id}: patch: requestBody: description: The drink to update. required: false content: application/json: schema: type: object properties: name: type: string description: The name of the drink. ingredients: type: array items: type: string description: The ingredients of the drink. instructions: type: string description: Instructions to prepare the drink. ``` Sometimes an API will set a request body for a DELETE request, but this is less common and recommended against in both [RFC9110: HTTP Semantics](https://www.rfc-editor.org/rfc/rfc9110.html#name-delete) and the OpenAPI specification. Still, OpenAPI begrudgingly allows `DELETE` to describe a `requestBody` knowing that some legacy APIs will have done this, and some tooling will even support it. ```yaml paths: /drinks/{id}: delete: requestBody: description: The drink to delete. required: false content: application/json: schema: type: object properties: reason: type: string description: The reason for deleting the drink. ``` It's important to describe the API correctly, regardless of which best practices it has gone against, but whenever possible channel the feedback to the developers and see if those mistakes can be avoided in the future. ## Encoding Object Only applicable to `requestBody` where the media type is `multipart` or `application/x-www-form-urlencoded`. An encoding object describes the encoding of a single property in the request schema. <Table title="Encoding Object Fields" data={[ { field: "`contentType`", type: "String", required: "", description: "The content type of the field. If the field is an `object`, the default is `application/json`. If the field is an array, the default is based on the inner type. Otherwise, the default is `application/octet-stream`. Valid values are either a media type (for example, `application/json`), a wildcard media type (for example, `image/*`), or a comma-separated list of media types and wildcard media types (for example, `image/png, application/*`)." }, { field: "`headers`", type: "Map[string, [Header Object](/openapi/paths/operations/responses/headers) | [Reference Object](/openapi/references#openapi-reference-object)]", required: "", description: "Only applies to `multipart` requests. Allows additional headers related to the field. For example, if the client needs to add a `Content-Disposition` for an uploaded file. A `Content-Type` header in this map will be ignored, in favor of the `contentType` field of the encoding object." }, { field: "`style`", type: "String", required: "", description: "Can take one of the following values: `form`, `spaceDelimited`, `pipeDelimited`, or `deepObject`. Specifies the style of the field's serialization only in requests with media type `multipart/form-data` or `application/x-www-form-urlencoded`. See the description of `style` under [Query Parameters](/openapi/paths/parameters/query-parameters)." }, { field: "`explode`", type: "Boolean", required: "", description: "Only applies to requests with media type `multipart/form-data` or `application/x-www-form-urlencoded` and fields with `array` or `object` types. If `style` is `form`, the default is `true`, otherwise the default is `false`." }, { field: "`allowReserved`", type: "Boolean", required: "", description: "Only applies to requests with media type `application/x-www-form-urlencoded`. Determines whether reserved characters (those allowed in literals but with reserved meanings) are allowed in the parameter's content. The default is `false`. When `true`, it allows reserved characters as defined by [RFC 3986](https://datatracker.ietf.org/doc/html/rfc3986#section-2.2) to be included without percent-encoding. This can be useful for parameters with content such as URLs." } ]} columns={[ { key: "field", header: "Field" }, { key: "type", header: "Type" }, { key: "required", header: "Required" }, { key: "description", header: "Description" } ]} /> ```yaml paths: /drinks: post: requestBody: description: The drink to create. required: true content: multipart/form-data: schema: properties: # ... other properties ... photo: description: A photo of the drink. type: string format: binary encoding: photo: contentType: image/jpeg, image/png headers: Content-Disposition: description: Specifies the disposition of the file (attachment and file name). schema: type: string default: 'form-data; name="photo"; filename="default.jpg"' allowReserved: false # style: form - not applicable to strings # explode: false - not applicable to strings ``` ## anyOf and oneOf Sometimes a request body could contain multiple different data structures: ```yaml requestBody: description: A JSON object containing pet information content: application/json: schema: oneOf: - $ref: "#/components/schemas/DebitCard" - $ref: "#/components/schemas/CreditCard" - $ref: "#/components/schemas/BankTransfer" - $ref: "#/components/schemas/IDEAL" ``` Learn more about this concept in the [schema composition](/openapi/schemas/composition) guide. # Parameters in OpenAPI Source: https://speakeasy.com/openapi/requests/parameters import { Table } from "@/mdx/components"; Parameters are used to describe inputs to an operation. Parameters can be defined at the path or operation level and are merged with any duplicates at the operation level, overriding any defined at the path level. Each parameter needs to be uniquely identified by a combination of its `name` and `in` fields in an [operation](/openapi/paths/operations). A parameter in the list can either be a [Parameter Object](/openapi/paths/parameters#parameter-object) or a [Reference](/openapi/references) to a [Parameter Object](/openapi/paths/parameters#parameter-object) defined in the [Components Object](/openapi/components) under the `parameters` field. Parameters can represent a number of different input types, including: - Path Parameters - Query Parameters - Headers - Cookies Example: ```yaml paths: /drinks/{type}: parameters: - name: type in: path description: The type of drink to filter by. required: true schema: $ref: "#/components/schemas/DrinkType" - name: Cache-Control in: header description: The cache control header. required: false schema: type: string enum: - no-cache - no-store - must-revalidate - max-age=0 - max-age=3600 - max-age=86400 - max-age=604800 - max-age=2592000 - max-age=31536000 get: operationId: listDrinks summary: Get a list of drinks. description: Get a list of drinks, if authenticated this will include stock levels and product codes otherwise it will only include public information. security: - {} tags: - drinks parameters: - name: limit in: query description: The maximum number of drinks to return. required: false schema: type: integer minimum: 1 maximum: 100 - name: filter in: query description: Advanced filter criteria as a JSON object. required: false content: application/json: schema: type: object properties: productCode: type: string inStock: type: boolean responses: "200": description: A list of drinks. content: application/json: schema: type: array items: $ref: "#/components/schemas/Drink" ``` ## Parameter Object <Table title="Parameter Object Fields" data={[ { field: "`name`", type: "String", required: "✅", description: "The **case sensitive** name of the parameter. This **_must_** be unique when combined with the `in` field.\nIf the `in` field is `path`, then this field **_must_** be referenced in the owning path." }, { field: "`in`", type: "String", required: "✅", description: "The type or location of the parameter. The available types are:\n- `path` - A templated parameter defined within the path.\n- `query` - A query parameter passed via the URL.\n- `header` - A header parameter passed via HTTP headers.\n- `cookie` - A cookie parameter passed via HTTP cookies." }, { field: "`description`", type: "String", required: "", description: "A description of the parameter. This may contain [CommonMark syntax](https://spec.commonmark.org/) to provide a rich description." }, { field: "`required`", type: "Boolean", required: "", description: "Whether the parameter is required. If the `in` field is `path`, then this field is **always** required and **_must_** be `true`. Defaults to `false`." }, { field: "`deprecated`", type: "Boolean", required: "", description: "Whether the parameter is deprecated. Defaults to `false`." }, { field: "`style`", type: "String", required: "", description: "Describes how the parameter value will be serialized depending on the `in` field. The available styles are `matrix`, `label`, `form`, `simple`, `spaceDelimited`, `pipeDelimited`, and `deepObject`.\n\nThe default style depends on the `in` field:\n- `path` - `simple`\n- `query` - `form`\n- `header` - `simple`\n- `cookie` - `form`\n\nSee the [path](https://www.speakeasy.com/openapi/paths/parameters/path-parameters), [header](https://www.speakeasy.com/openapi/paths/parameters/header-parameters), [query](https://www.speakeasy.com/openapi/paths/parameters/query-parameters), and [cookie](https://www.speakeasy.com/openapi/paths/parameters/cookie-parameters) parameter sections for more details." }, { field: "`explode`", type: "Boolean", required: "", description: "Whether the parameter value will be exploded, based on the parameter type. Defaults to `true` when `style` is `form`, otherwise `false`.\n\nSee the [path](https://www.speakeasy.com/openapi/paths/parameters/path-parameters), [header](https://www.speakeasy.com/openapi/paths/parameters/header-parameters), [query](https://www.speakeasy.com/openapi/paths/parameters/query-parameters), and [cookie](https://www.speakeasy.com/openapi/paths/parameters/cookie-parameters) parameter sections for more details." }, { field: "`schema`", type: "[Schema Object](/openapi/schemas)", required: "", description: "A schema or reference to a schema that defines the type of the parameter. This is **_required_** unless `content` is defined.\n\n**Note: OpenAPI 3.0.x supports [OpenAPI Reference Objects](/openapi/references#openapi-reference-object) here as the value. OpenAPI 3.1.x uses the [JSON Schema Referencing](/openapi/schemas#json-schema--openapi) format.**" }, { field: "`content`", type: "[Content](/openapi/paths/operations/content)", required: "", description: "A map of [Media Type Objects](/openapi/paths/operations/content#media-type-object) that defines the possible media types that can be used for the parameter. This is **_required_** unless `schema` is defined." }, { field: "`allowEmptyValue`", type: "Boolean", required: "", description: "Whether the parameter value can be empty. Only used if `in` is `query`. Defaults to `false`." }, { field: "`allowReserved`", type: "Boolean", required: "", description: "Whether the parameter value can contain reserved characters as defined by [RFC 3986](https://www.rfc-editor.org/rfc/rfc3986). Only used if `in` is `query`. Defaults to `false`." }, { field: "`example`", type: "Any", required: "", description: "An example of the parameter's value. This is ignored if the `examples` field is defined." }, { field: "`examples`", type: "[Examples](/openapi/examples)", required: "", description: "A map of [Example Objects](/openapi/examples) and/or [OpenAPI Reference Objects](/openapi/references#openapi-reference-object) that define the possible examples of the parameter's value." }, { field: "`x-*`", type: "[Extensions](/openapi/extensions)", required: "", description: "Any number of extension fields can be added to the parameter object that can be used by tooling and vendors." } ]} columns={[ { key: "field", header: "Field" }, { key: "type", header: "Type" }, { key: "required", header: "Required" }, { key: "description", header: "Description" } ]} /> The order of fields above is recommended for defining fields in the document. ## Parameter Serialization OpenAPI provides two mutually exclusive ways to describe how parameter values are serialized: using `schema` with `style` and `explode`, or using `content`. You must use one or the other, but not both. ### Schema with Style and Explode (Recommended for Most Cases) The `schema` approach is the standard way to define parameters and is suitable for most scenarios. It works well for primitive values, arrays, and simple objects that can be serialized into a string. With this approach, serialization is controlled by: - **`style`**: Defines the serialization format (e.g., `form`, `simple`, `matrix`, `label`) - **`explode`**: Controls whether arrays and objects are expanded into separate parameters Example of array serialization using `schema`: ```yaml parameters: - name: colors in: query schema: type: array items: type: string style: form explode: false ``` This serializes as: `?colors=blue,black,brown` For detailed serialization rules for each parameter type, see the [path](/openapi/requests/parameters/path-parameters), [header](/openapi/requests/parameters/header-parameters), [query](/openapi/requests/parameters/query-parameters), and [cookie](/openapi/requests/parameters/cookie-parameters) parameter documentation. ### Content (For Complex Serialization) The `content` approach is designed for complex serialization scenarios that cannot be handled by `style` and `explode`. This is particularly useful when you need to send structured data using a specific media type serialization, such as JSON. Use `content` when you need to: - Send complex nested objects that require JSON serialization - Use a specific media type format for the parameter value - Handle serialization that goes beyond what `style` and `explode` support Example of JSON-serialized object in a query parameter: ```yaml parameters: - name: filter in: query description: Filter criteria as a JSON object required: false content: application/json: schema: type: object properties: type: type: string example: t-shirt color: type: string example: blue priceRange: type: object properties: min: type: number max: type: number ``` This parameter would be sent as a JSON string in the URL: ``` ?filter={"type":"t-shirt","color":"blue","priceRange":{"min":10,"max":50}} ``` The `content` field maps media types to schemas, allowing you to specify exactly how the parameter should be serialized and deserialized. While `application/json` is common for complex query parameters, you can use any media type that makes sense for your API. **Important**: You cannot use `style` and `explode` when using `content`. Choose the approach that best fits your serialization needs. # Cookie Parameters in OpenAPI Source: https://speakeasy.com/openapi/requests/parameters/cookie-parameters import { Table } from "@/mdx/components"; Cookie parameters are serialized at runtime to an HTTP cookie header. Types are generally serialized to a string representation, and only `form` style is available. Currently, cookies are not well supported by OpenAPI and this may change in the future, so using the default `style: form` and `explode: true` values results in serialization incompatible with most cookie parsers. Therefore, it is recommended to only use cookies for primitive types or arrays with `explode: false`, but the current serialization behaviors are included below for completeness. If using cookies for authentication, it is recommended to use the OpenAPI [`security`](/openapi/security) field to document a security scheme instead of a cookie parameter. ## Primitive Types As Cookies in OpenAPI Primitive types such as `string`, `number`, `integer`, and `boolean` are serialized as a string. For the example below, we will use a cookie parameter named `drink-limit` with a value of `5`. <Table title="Primitive Types Serialization" data={[ { style: "`form`", explodeTrue: "`Cookie: drink-limit=5` (default)", explodeFalse: "`Cookie: drink-limit=5`" } ]} columns={[ { key: "style", header: "Style" }, { key: "explodeTrue", header: "Explode == `true`" }, { key: "explodeFalse", header: "Explode == `false`" } ]} /> ## Simple Arrays As Cookies in OpenAPI For simple arrays of primitive types such as `string`, `number`, `integer`, and `boolean`, serialization will vary depending on the `explode` field. For the example below, we will use a cookie parameter named `drink-types` with a value of `["gin", "vodka", "rum"]`. <Table title="Simple Arrays Serialization" data={[ { style: "`form`", explodeTrue: "`Cookie: drink-types=gin&drink-types=vodka&drink-types=rum` (default)", explodeFalse: "`Cookie: drink-types=gin,vodka,rum`" } ]} columns={[ { key: "style", header: "Style" }, { key: "explodeTrue", header: "Explode == `true`" }, { key: "explodeFalse", header: "Explode == `false`" } ]} /> ## Simple Objects As Cookies in OpenAPI For simple objects whose fields are primitive types such as `string`, `number`, `integer`, and `boolean`, serialization will vary depending on the `explode` field. For the example below, we will use a cookie parameter named `drink-filter` with a value of `{"type": "cocktail", "strength": 5}`. <Table title="Simple Objects Serialization" data={[ { style: "`form`", explodeTrue: "`Cookie: type=cocktail&strength=5` (default)", explodeFalse: "`Cookie: drink-filter=type,cocktail,strength,5`" } ]} columns={[ { key: "style", header: "Style" }, { key: "explodeTrue", header: "Explode == `true`" }, { key: "explodeFalse", header: "Explode == `false`" } ]} /> ## Complex Objects and Arrays As Cookies in OpenAPI For complex objects and arrays, serialization in a cookie parameter is only really possible using `content` and not any `style` options. For example, to serialize using JSON, the following: ```yaml parameters: - name: drink-filter in: cookie content: application/json: schema: type: object properties: type: type: array items: type: string strength: type: array items: type: integer ``` Would serialize to `Cookie: drink-filter={"type":["cocktail","mocktail"],"strength":[5,10]}`. # OpenAPI Header Parameters Source: https://speakeasy.com/openapi/requests/parameters/header-parameters import { Table } from "@/mdx/components"; Header parameters are serialized at runtime to the HTTP headers of the request. Types are generally serialized to a string representation, and only `simple` style is available. Explode defaults to `false`. There are a few reserved headers that cannot be used as parameter names and are enabled by other OpenAPI features: - `Accept` - Defining content types in the [Response Object](/openapi/paths/operations/responses#response-object) `content` field, documents the available values for the `Accept` header. - `Authorization` - Defining security requirements in the [Security Requirement Object](/openapi/security#security-requirement-object) `security` field, documents that the `Authorization` header is required. - `Content-Type` - Defining content types in the [Request Body Object](/openapi/paths/operations/requests) `content` field, documents that the `Content-Type` header is required and the acceptable values. If using headers for authentication, it is recommended to use the OpenAPI [`security`](/openapi/security) field to document a security scheme instead of a header parameter. ## Primitive Types As Headers in OpenAPI Primitive types such as `string`, `number`, `integer`, and `boolean` are serialized as a string. For the example below, we will use a header parameter named `X-Drink-Limit` with a value of `5`. <Table title="Primitive Types Serialization" data={[ { style: "`simple`", explodeTrue: "`X-Drink-Type: 5`", explodeFalse: "`X-Drink-Type: 5` (default)" } ]} columns={[ { key: "style", header: "Style" }, { key: "explodeTrue", header: "Explode == `true`" }, { key: "explodeFalse", header: "Explode == `false`" } ]} /> ## Simple Arrays As Headers in OpenAPI For simple arrays of primitive types such as `string`, `number`, `integer`, and `boolean`, the `style` and `explode` fields have little effect on the serialization. For the example below, we will use a header parameter named `X-Drink-Types` with a value of `["gin", "vodka", "rum"]`. <Table title="Simple Arrays Serialization" data={[ { style: "`simple`", explodeTrue: "`X-Drink-Type: gin,vodka,rum`", explodeFalse: "`X-Drink-Type: gin,vodka,rum` (default)" } ]} columns={[ { key: "style", header: "Style" }, { key: "explodeTrue", header: "Explode == `true`" }, { key: "explodeFalse", header: "Explode == `false`" } ]} /> ## Simple Objects As Headers in OpenAPI For simple objects whose fields are primitive types such as `string`, `number`, `integer`, and `boolean`, serialization will vary depending on the `explode` field. For the example below, we will use a header parameter named `X-Drink-Filter` with a value of `{"type": "cocktail", "strength": 5}`. <Table title="Simple Objects Serialization" data={[ { style: "`simple`", explodeTrue: "`X-Drink-Type: type=cocktail,strength=5`", explodeFalse: "`X-Drink-Type: type,cocktail,strength,5` (default)" } ]} columns={[ { key: "style", header: "Style" }, { key: "explodeTrue", header: "Explode == `true`" }, { key: "explodeFalse", header: "Explode == `false`" } ]} /> ## Complex Objects and Arrays As Headers in OpenAPI For complex objects and arrays, serialization in a header parameter is only really possible using `content` and not any `style` options. For example, to serialize using JSON, the following: ```yaml parameters: - name: X-Drink-Filter in: header content: application/json: schema: type: object properties: type: type: array items: type: string strength: type: array items: type: integer ``` Would serialize to `X-Drink-Filter: {"type":["cocktail","mocktail"],"strength":[5,10]}`. # OpenAPI path parameters Source: https://speakeasy.com/openapi/requests/parameters/path-parameters import { Table } from "@/mdx/components"; Path parameters are serialized at runtime to the path of the URL, meaning they are generally serialized to a string representation and must adhere to the [RFC 3986](https://www.rfc-editor.org/rfc/rfc3986) specification. Reserved characters are percent-encoded (for example, `?` becomes `%3F`). By default, path parameters are serialized using `style: simple` and `explode: false`, but several different serialization options are available: - `style: simple` - Simple style serialization is the default serialization for path parameters, using commas (`,`) to separate multiple values. Defined by [RFC 6570](https://tools.ietf.org/html/rfc6570#section-3.2.7). - `style: label` - Label-style serialization uses dots (`.`) to separate multiple values. Defined by [RFC 6570](https://tools.ietf.org/html/rfc6570#section-3.2.6). - `style: matrix` - Matrix-style serialization uses semicolons (`;`) to separate multiple values. Defined by [RFC 6570](https://tools.ietf.org/html/rfc6570#section-3.2.5). ## Primitive types as path parameters in OpenAPI Primitive types such as `string`, `number`, `integer`, and `boolean` are serialized as strings. The `style` and `explode` fields generally determine the prefix for the value. For the examples below, we will use a path parameter named `type` with a value of `cocktail` for a path-templated URL of `/drinks/{type}`. <Table title="Primitive Types Serialization" data={[ { style: "`simple`", explodeTrue: "/drinks/cocktail", explodeFalse: "/drinks/cocktail" }, { style: "`label`", explodeTrue: "/drinks/.cocktail", explodeFalse: "/drinks/.cocktail" }, { style: "`matrix`", explodeTrue: "/drinks/;type=cocktail", explodeFalse: "/drinks/;type=cocktail" } ]} columns={[ { key: "style", header: "Style" }, { key: "explodeTrue", header: "Explode == `true`" }, { key: "explodeFalse", header: "Explode == `false`" } ]} /> ## Simple arrays as path parameters in OpenAPI For simple arrays of primitive types such as `string`, `number`, `integer`, and `boolean`, serialization will vary depending on the `style` and `explode` fields. For the examples below, we will use a path parameter named `types` with a value of `["gin", "vodka", "rum"]` for a path-templated URL of `/drinks/{types}`. <Table title="Simple Arrays Serialization" data={[ { style: "`simple`", explodeTrue: "/drinks/gin,vodka,rum", explodeFalse: "/drinks/gin,vodka,rum (default)" }, { style: "`label`", explodeTrue: "/drinks/.gin.vodka.rum", explodeFalse: "/drinks/.gin,vodka,rum" }, { style: "`matrix`", explodeTrue: "/drinks/;types=gin;types=vodka;types=rum", explodeFalse: "/drinks/;types=gin,vodka,rum" } ]} columns={[ { key: "style", header: "Style" }, { key: "explodeTrue", header: "Explode == `true`" }, { key: "explodeFalse", header: "Explode == `false`" } ]} /> ## Simple objects as path parameters in OpenAPI For simple objects whose fields are primitive types such as `string`, `number`, `integer`, and `boolean`, serialization will vary depending on the `style` and `explode` fields. For the examples below, we will use a path parameter named `filter` with a value of `{"type": "cocktail", "strength": 5}` for a path-templated URL of `/drinks/{filter}`. <Table title="Simple Objects Serialization" data={[ { style: "`simple`", explodeTrue: "/drinks/type=cocktail,strength=5", explodeFalse: "/drinks/type,cocktail,strength,5 (default)" }, { style: "`label`", explodeTrue: "/drinks/.type=cocktail.strength=5", explodeFalse: "/drinks/.type,cocktail,strength,5" }, { style: "`matrix`", explodeTrue: "/drinks/;type=cocktail;strength=5", explodeFalse: "/drinks/;filter=type,cocktail,strength,5" } ]} columns={[ { key: "style", header: "Style" }, { key: "explodeTrue", header: "Explode == `true`" }, { key: "explodeFalse", header: "Explode == `false`" } ]} /> ## Complex objects and arrays as path parameters in OpenAPI For complex objects and arrays, serialization in a path parameter is only possible using `content` and not any `style` options. For example, to serialize using JSON, you can define a parameter in the following way: ```yaml parameters: - name: filter in: path content: application/json: schema: type: object properties: type: type: array items: type: string strength: type: array items: type: integer ``` This configuration would serialize to `/drinks/%7B%22type%22%3A%5B%22cocktail%22%2C%22mocktail%22%5D%2C%22strength%22%3A%5B5%2C10%5D%7D`, which is the equivalent of `/drinks/{"type":["cocktail","mocktail"],"strength":[5,10]}` unencoded. ## How to override path parameter encoding By default, the characters `:/?#[]@!$&'()*+,;=` are encoded when present in the value of a path parameter. To render these characters unencoded in a request URL, use the `x-speakeasy-param-encoding-override: allowReserved` extension. Read more about parameter encoding in the ([docs](/docs/customize/java/param-encoding)). # OpenAPI Query Parameters Source: https://speakeasy.com/openapi/requests/parameters/query-parameters import { Table } from "@/mdx/components"; Query parameters are serialized at runtime to the query string of the URL, meaning they are generally serialized to a string representation and must adhere to the [RFC 3986](https://www.rfc-editor.org/rfc/rfc3986) specification. By default, reserved characters are percent-encoded (for example, `?` becomes `%3F`) but this can be disabled by setting `allowReserved` to `true`. By default, query parameters are serialized using `style: form` and `explode: true` but there are a number of different serialization options available: - `style: form` - Form style serialization is the default serialization for query parameters. It generally uses ampersands (`&`) to separate multiple values and equals (`=`) to separate the key and value. Defined by [RFC 6570](https://tools.ietf.org/html/rfc6570#section-3.2.8). - `style: pipeDelimited` - Pipe-delimited serialization uses pipes (`|`) to separate multiple values. - `style: spaceDelimited` - Space-delimited serialization uses percent-encoded spaces (`%20`) to separate multiple values. - `style: deepObject` - Deep-object serialization uses nested objects to represent the parameter value. ## Primitive Types As Query Parameters in OpenAPI For primitive types such as `string`, `number`, `integer,` and `boolean`, the serialization is straightforward and the value is serialized as a string. The `style` and `explode` fields have little effect on the serialization. For the examples below, we will use a query parameter named `limit` with a value of `10`. <Table title="Primitive Types Serialization" data={[ { style: "`form`", explodeTrue: "/query?limit=10 (default)", explodeFalse: "/query?limit=10" }, { style: "`pipeDelimited`", explodeTrue: "/query?limit=10", explodeFalse: "/query?limit=10" }, { style: "`spaceDelimited`", explodeTrue: "/query?limit=10", explodeFalse: "/query?limit=10" }, { style: "`deepObject`", explodeTrue: "**NOT VALID**", explodeFalse: "**NOT VALID**" } ]} columns={[ { key: "style", header: "Style" }, { key: "explodeTrue", header: "Explode == `true`" }, { key: "explodeFalse", header: "Explode == `false`" } ]} /> ## Simple Arrays As Query Parameters in OpenAPI For simple arrays of primitive types such as `string`, `number`, `integer`, and `boolean`, serialization will vary depending on the `style` and `explode` fields. For the examples below, we will use a query parameter named `terms` with a value of `["gin", "vodka", "rum"]`. <Table title="Simple Arrays Serialization" data={[ { style: "`form`", explodeTrue: "/query?terms=gin&terms=vodka&terms=rum (default)", explodeFalse: "/query?terms=gin,vodka,rum" }, { style: "`pipeDelimited`", explodeTrue: "/query?terms=gin&terms=vodka&terms=rum", explodeFalse: "/query?terms=gin|vodka|rum" }, { style: "`spaceDelimited`", explodeTrue: "/query?terms=gin&terms=vodka&terms=rum", explodeFalse: "/query?terms=gin%20vodka%20rum" }, { style: "`deepObject`", explodeTrue: "**NOT VALID**", explodeFalse: "**NOT VALID**" } ]} columns={[ { key: "style", header: "Style" }, { key: "explodeTrue", header: "Explode == `true`" }, { key: "explodeFalse", header: "Explode == `false`" } ]} /> ## Simple Objects As Query Parameters in OpenAPI For simple objects whose fields are primitive types such as `string`, `number`, `integer`, and `boolean`, serialization will vary depending on the `style` and `explode` fields. For the examples below, we will use a query parameter named `filter` with a value of `{"type": "cocktail", "strength": 5}`. <Table title="Simple Objects Serialization" data={[ { style: "`form`", explodeTrue: "/query?type=cocktail&strength=5 (default)", explodeFalse: "/query?filter=type,cocktail,strength,5" }, { style: "`pipeDelimited`", explodeTrue: "/query?type=cocktail&strength=5", explodeFalse: "/query?filter=type|cocktail|strength|5" }, { style: "`spaceDelimited`", explodeTrue: "/query?type=cocktail&strength=5", explodeFalse: "/query?filter=type%20cocktail%20strength%205" }, { style: "`deepObject`", explodeTrue: "**Not applicable**", explodeFalse: "/query?filter[type]=cocktail&filter[strength]=5" } ]} columns={[ { key: "style", header: "Style" }, { key: "explodeTrue", header: "Explode == `true`" }, { key: "explodeFalse", header: "Explode == `false`" } ]} /> The `style: deepObject` serialization uses bracket notation and the `explode` parameter is not applicable (it is ignored regardless of whether it is set to `true`, `false`, or omitted). There is a special case for simple objects with fields that are an array of primitive types such as `string`, `number`, `integer`, and `boolean`. For example, for a query parameter named `filter` with a value of `{"type": ["cocktail", "mocktail"], "strength": [5, 10]}`, this will be serialized like `/query?filter[type]=cocktail&filter[type]=mocktail&filter[strength]=5&filter[strength]=10`. ## Complex Objects and Arrays As Query Parameters in OpenAPI For complex objects and arrays, serialization in a query parameter is only really possible using `content` and not any `style` options. For example, to serialize using JSON, the following: ```yaml parameters: - name: filter in: query content: application/json: schema: type: object properties: type: type: array items: type: string strength: type: array items: type: integer ``` Would serialize to `/query?filter=%7B%22type%22%3A%5B%22cocktail%22%2C%22mocktail%22%5D%2C%22strength%22%3A%5B5%2C10%5D%7D`, which is the equivalent of `/query?filter={"type":["cocktail","mocktail"],"strength":[5,10]}` unencoded. # OpenAPI Response Objects Source: https://speakeasy.com/openapi/responses import { Table } from "@/mdx/components"; The Responses Object is a map of [Response Objects](/openapi/paths/operations/responses#response-object) or [References](/openapi/references) to [Response Objects](/openapi/paths/operations/responses#response-object) that define the possible responses that can be returned from executing the operation. The keys in the map represent any known HTTP status codes that the API may return. The HTTP status codes can be defined like below: - Numeric Status Code - for example, `200`, `404`, or `500`. HTTP status codes are defined in [RFC 9110](https://httpwg.org/specs/rfc9110.html#overview.of.status.codes). - Status Code Wildcards - for example, `1XX`, `2XX`, `3XX`, `4XX`, or `5XX`. A wildcard that matches any status code in the range of its significant digit, for example, `2XX` represents status codes `200` to `299` inclusive. - `default` - A catch-all identifier for any other status codes not defined in the map. The map **_must_** contain at least one successful response code. All values **_must_** be defined as explicit strings (for example,`"200"`) to allow for compatibility between JSON and YAML. For example: ```yaml paths: /drinks: get: operationId: listDrinks summary: Get a list of drinks. description: Get a list of drinks, if authenticated this will include stock levels and product codes otherwise it will only include public information. tags: - drinks parameters: - name: type in: query description: The type of drink to filter by. If not provided all drinks will be returned. required: false schema: $ref: "#/components/schemas/DrinkType" responses: "200": description: A list of drinks. content: application/json: schema: type: array items: $ref: "#/components/schemas/Drink" "5XX": description: An error occurred interacting with the API. content: application/json: schema: $ref: "#/components/schemas/APIError" default: description: An unknown error occurred interacting with the API. content: application/json: schema: $ref: "#/components/schemas/Error" ``` Any number of [extension](/openapi/extensions) fields can be added to the responses object that can be used by tooling and vendors. ## Response Object in OpenAPI The Response Object describes a single response that can be returned from executing an [operation](/openapi/paths/operations). <Table title="Response Object Fields" data={[ { field: "`description`", type: "String", required: "✅", description: "A description of the response. This may contain [CommonMark syntax](https://spec.commonmark.org/) to provide a rich description." }, { field: "`headers`", type: "[Headers](/openapi/paths/operations/responses/headers)", required: "", description: "A map of [Header Objects](/openapi/paths/operations/responses/headers) that defines the headers that can be returned from executing this operation." }, { field: "`content`", type: "[Content](/openapi/paths/operations/content)", required: "", description: "A map of [Media Type Objects](/openapi/paths/operations/content#media-type-object) that defines the possible media types that can be returned from executing this operation." }, { field: "`links`", type: "[Links](/openapi/paths/operations/responses/links)", required: "", description: "A map of [Link Objects](/openapi/paths/operations/responses/links#link-object) or [References](/openapi/references) that define the possible links that can be returned from executing this operation." }, { field: "`x-*`", type: "[Extensions](/openapi/extensions)", required: "", description: "Any number of extension fields can be added to the response object that can be used by tooling and vendors." } ]} columns={[ { key: "field", header: "Field" }, { key: "type", header: "Type" }, { key: "required", header: "Required" }, { key: "description", header: "Description" } ]} /> # Errors in OpenAPI Source: https://speakeasy.com/openapi/responses/errors import { Table, Callout } from "@/mdx/components"; In OpenAPI, errors are typically represented as [responses](/openapi/paths/operations/responses#response-object-in-openapi), with a clear body response structure and a status code in the range of **400-599** describing the error. A well-designed error response should provide a clear and actionable message that helps the developer describe the error to a user. ## Key elements of an error response The status code and response body are the key elements of an error response. ### Status code HTTP response status codes play a vital role in describing the nature of errors. You should use appropriate codes to indicate the error type: <Table data={[ { statusCode: "400", meaning: "Bad Request", exampleUseCase: "Input validation failed" }, { statusCode: "401", meaning: "Unauthorized", exampleUseCase: "Missing or invalid authentication" }, { statusCode: "403", meaning: "Forbidden", exampleUseCase: "Insufficient permissions" }, { statusCode: "404", meaning: "Not Found", exampleUseCase: "Resource does not exist" }, { statusCode: "413", meaning: "Payload Too Large", exampleUseCase: "File or request body exceeds limit" }, { statusCode: "415", meaning: "Unsupported Media Type", exampleUseCase: "Invalid content type (for example, text or XML)" }, { statusCode: "500", meaning: "Internal Server Error", exampleUseCase: "Unexpected server-side issue" } ]} columns={[ { key: "statusCode", header: "Status code" }, { key: "meaning", header: "Meaning" }, { key: "exampleUseCase", header: "Example use case" } ]} /> <Callout title="HTTP response status codes" type="info"> You can find a comprehensive list of HTTP response status codes for client errors (400-499) [here](https://developer.mozilla.org/en-US/docs/Web/HTTP/Status#client_error_responses) and server errors (500-599) [here](https://developer.mozilla.org/en-US/docs/Web/HTTP/Status#server_error_responses). </Callout> ### Response body The response body provides the client with additional information about the error that can be displayed to the user. It is recommended to use a structured schema that describes the error in a clear and actionable way. Here is an example: ```yaml { "error": "ValidationError", "message": "The provided file type is not supported.", "details": { "field": "fileType", "allowedValues": ["image/jpeg", "application/pdf"] } } ``` An error response can have these common properties: - `error`: A machine-readable error code, such as `ValidationError` or `AuthenticationFailed`. - `message`: A human-readable description of the error. - `details`: Additional contextual information about the error, such as invalid fields or allowed values. You can provide additional properties in the response body, such as the `timestamp` or `trace ID`, to help with debugging and tracking the error. ## Defining error responses in an OpenAPI document For standard API endpoints, error responses should be defined in the `responses` section of the OpenAPI document. You can use reusable components to standardize error schemas: ```yaml filename="openapi.yaml" paths: /resource: get: summary: Retrieve a resource responses: '200': description: Successful operation '400': $ref: '#/components/responses/BadRequestError' '500': $ref: '#/components/responses/InternalServerError' components: responses: BadRequestError: description: Invalid input provided content: application/json: schema: $ref: '#/components/schemas/ErrorResponse' InternalServerError: description: Unexpected server error content: application/json: schema: $ref: '#/components/schemas/ErrorResponse' schemas: ErrorResponse: type: object properties: error: type: string description: Machine-readable error code message: type: string description: Human-readable error message details: type: object additionalProperties: true description: Additional error context timestamp: type: string format: date-time traceId: type: string description: Unique identifier for error tracing ``` ## RFC 9457 Problem Details for Errors ## Standardizing error messages: RFC 9457 Problem Details [RFC 9457](https://tools.ietf.org/html/rfc9457) is a standard for representing errors in REST APIs. Published in July 2023, it introduces a standardized, machine-readable, and actionable format for describing errors. While RFC 9457 is widely regarded as a best practice, it is important to note that it is not included in the OpenAPI Specification (OAS). The Problem Details format for describing errors is JSON-based and specifies key properties that are either required, optional, or extensible. When serialized as JSON, the format uses the media type `application/problem+json`. The core properties of the Problem Details format are: - `type`: A URI reference that identifies the problem type, providing documentation or additional context for the error. - `title`: A short, human-readable summary of the problem type. The title should be consistent across instances of the same problem. - `status`: The HTTP status code for the error. - `detail`: A human-readable explanation of the error. - `instance`: A URI reference that identifies the specific occurrence of the problem, providing a link for debugging or accessing more information. Here is how you can apply RFC 9457 to your OpenAPI documentation: ```yaml filename="openapi.yaml" paths: /user: post: summary: Create a new user responses: '400': description: Validation Error content: application/problem+json: schema: $ref: '#/components/schemas/ProblemDetails' components: schemas: ProblemDetails: type: object properties: type: type: string format: uri-reference description: URI reference identifying the problem type. title: type: string description: A short, human-readable summary of the problem type. status: type: integer description: HTTP status code for this error occurrence. detail: type: string description: Explanation specific to this occurrence of the problem. instance: type: string format: uri-reference description: URI reference identifying the specific occurrence of the problem. ``` # OpenAPI Headers Source: https://speakeasy.com/openapi/responses/headers import { Table } from "@/mdx/components"; A map of header names to [Header Objects](/openapi/paths/operations/responses/headers) or [References](/openapi/references) that define headers in [Response Objects](/openapi/paths/operations/responses#response-object) or [Encoding Objects](/openapi/paths/operations/requests#encoding-object). In this simplified example, the server returns three [Header Objects](/openapi/paths/operations/responses/headers) with the names `X-RateLimit-Remaining`, `Last-Modified`, and `Cache-Control`: ```yaml paths: /drinks/{productCode}: get: responses: "200" description: A drink. content: application/json: schema: $ref: "#/components/schemas/Drink" headers: X-RateLimit-Remaining: description: The number of requests left for the time window. schema: type: integer example: 99 Last-Modified: description: The time at which the information was last modified. schema: type: string format: date-time example: '2024-01-26T18:25:43.511Z' Cache-Control: description: Instructions for caching mechanisms in both requests and responses. schema: type: string example: no-cache ``` ## Header Object in OpenAPI Describes a single header. The name of a header is determined by the header's key in a `headers` map. <Table title="Header Object Fields" data={[ { field: "`description`", type: "String", required: "", description: "A description of the header. This may contain [CommonMark syntax](https://spec.commonmark.org/) to provide a rich description." }, { field: "`required`", type: "Boolean", required: "", description: "Whether the header is required. Defaults to `false`." }, { field: "`deprecated`", type: "Boolean", required: "", description: "Whether the header is deprecated. Defaults to `false`." }, { field: "`schema`", type: "[Schema Object](/openapi/schemas)", required: "", description: "A schema or reference to a schema that defines the type of the header. This is **_required_** unless `content` is defined.\n\n**Note: OpenAPI 3.0.x supports [OpenAPI Reference Objects](/openapi/references#openapi-reference-object) here as a value. OpenAPI 3.1.x uses the [JSON Schema Referencing](/openapi/schemas#json-schema--openapi) format.**" }, { field: "`content`", type: "Map[string, [Media Type Object](/openapi/paths/operations/content#media-type-object)]", required: "", description: "A map of [Media Type Objects](/openapi/paths/operations/content#media-type-object) that define the possible media types that can be used for the header. This is **_required_** unless `schema` is defined." }, { field: "`x-*`", type: "[Extensions](/openapi/extensions)", required: "", description: "Any number of extension fields can be added to the header object to be used by tooling and vendors." } ]} columns={[ { key: "field", header: "Field" }, { key: "type", header: "Type" }, { key: "required", header: "Required" }, { key: "description", header: "Description" } ]} /> # OpenAPI Links Source: https://speakeasy.com/openapi/responses/links import { Table } from "@/mdx/components"; The Links object is a map of [Link Objects](/openapi/paths/operations/responses/links#link-object) or [References](/openapi/references) to [Link Objects](/openapi/paths/operations/responses/links#link-object) that allows for describing possible API-use scenarios between different operations. For example, if a response returns a `Drink` object, and the `Drink` object has an `ingredients` property that is a list of `Ingredient` objects, then a link can be defined to the `listIngredients` operation showing how the ingredients can be used as an input to the `listIngredients` operation. For example: ```yaml /drink/{name}: get: operationId: getDrink summary: Get a drink. description: Get a drink by name, if authenticated this will include stock levels and product codes otherwise it will only include public information. tags: - drinks parameters: - name: name in: path required: true schema: type: string responses: responses: "200": description: A drink. content: application/json: schema: $ref: "#/components/schemas/Drink" links: listIngredients: operationId: listIngredients parameters: ingredients: $response.body#/ingredients description: The list of ingredients returned by the `getDrink` operation can be used as an input to the `listIngredients` operation, to retrieve additional details about the ingredients required to make the drink. /ingredients: get: operationId: listIngredients summary: Get a list of ingredients. description: Get a list of ingredients, if authenticated this will include stock levels and product codes otherwise it will only include public information. tags: - ingredients parameters: - name: ingredients in: query description: A list of ingredients to filter by. If not provided all ingredients will be returned. required: false style: form explode: false schema: type: array items: type: string responses: "200": description: A list of ingredients. content: application/json: schema: type: array items: $ref: "#/components/schemas/Ingredient" "5XX": $ref: "#/components/responses/APIError" default: $ref: "#/components/responses/UnknownError" ``` ## Link Object in OpenAPI The Link Object represents a possible link that can be followed from the response. <Table title="Link Object Fields" data={[ { field: "`operationId`", type: "String", required: "✅", description: "The `operationId` of an [operation](/openapi/paths/operations) that exists in the document. Use either this field or the `operationRef` field, not both." }, { field: "`operationRef`", type: "String", required: "✅", description: "Either a [Relative Reference](/openapi/references#relative-references) or [Absolute Reference](/openapi/references#absolute-references) to an [operation](/openapi/paths/operations) that exists in the document. Use either this field or the `operationId` field, not both." }, { field: "`description`", type: "String", required: "", description: "A description of the link and intentions for its use. This may contain [CommonMark syntax](https://spec.commonmark.org/) to provide a rich description." }, { field: "`parameters`", type: "Map[string, any \\| [{Expression}](/openapi/references#runtime-expression)]", required: "", description: "A map of parameters to pass to the linked operation. The key is the name of the parameter and the value is either a constant value or an [Expression](/openapi/references#runtime-expression) that will be evaluated.\n\nThe parameter name can also be qualified with the location of the parameter, for example, `path.parameter_name` or `query.parameter_name`" }, { field: "`requestBody`", type: "Any \\| [{Expression}](/openapi/references#runtime-expression)", required: "", description: "A constant value or [Expression](/openapi/references#runtime-expression) that will be used as the request body when calling the linked operation." }, { field: "`server`", type: "[Server Object](/openapi/servers)", required: "", description: "An optional server to be used by the linked operation." }, { field: "`x-*`", type: "[Extensions](/openapi/extensions)", required: "", description: "Any number of extension fields can be added to the link object that can be used by tooling and vendors." } ]} columns={[ { key: "field", header: "Field" }, { key: "type", header: "Type" }, { key: "required", header: "Required" }, { key: "description", header: "Description" } ]} /> An example of `OperationRef`: ```yaml links: listIngredients: operationRef: "#/paths/~1ingredients/get" parameters: ingredients: $response.body#/ingredients # or links: listIngredients: operationRef: "https://speakeasy.bar/#/paths/~1ingredients/get" parameters: ingredients: $response.body#/ingredients ``` # Rate limiting in OpenAPI Source: https://speakeasy.com/openapi/responses/rate-limiting Rate limiting is the art of trying to protect the API by telling "overactive" API consumers to calm down a bit, telling clients to reduce the frequency of their requests, or take a break entirely and come back later to avoid overwhelming the API. **Learn more about rate limiting [over here](/api-design/rate-limiting).** ## Rate limiting error response When a client exceeds the rate limit, the API should respond with a `429 Too Many Requests` status code. This status code indicates that the user has sent too many requests in a given amount of time, and the error message can explain that more clearly, especially when using [quality error responses](/api-design/errors). ```yaml responses: "429": description: Too Many Requests content: application/problem+json: schema: type: object properties: type: type: string const: "https://example.com/probs/limit-exceeded" title: type: string const: Rate limit exceeded detail: type: string const: Too many requests in a given amount of time, please try again later. ``` This response describes an error that looks like this: ```http HTTP/2 429 Too Many Requests Content-Type: application/json { "type": "https://example.com/probs/limit-exceeded", "title": "Rate limit exceeded", "detail": "Too many requests in a given amount of time, please try again later." } ``` Describing the `429` response in OpenAPI is important for communicating to API consumers that there is rate limiting in place, if its been missed elsewhere or if its only available on some endpoints, this is a good chance to make it clear. ## Retry-After header The `Retry-After` header is a standard HTTP header that can be included in the response to indicate how long the client should wait before making another request. This header can be included in the `429 Too Many Requests` response to inform the client about the time they should wait before retrying. ```yaml responses: "429": description: Too Many Requests headers: Retry-After: description: The number of seconds to wait before making another request. schema: type: integer example: 3600 content: application/problem+json: schema: type: object properties: type: type: string const: "https://example.com/probs/limit-exceeded" title: type: string const: Rate limit exceeded detail: type: string const: Too many requests in a given amount of time, please try again in one hour. ``` With this header added and a more specific error detail added, the response looks more like this. ```http HTTP/2 429 Too Many Requests Content-Type: application/json Retry-After: 3600 { "type": "https://example.com/probs/limit-exceeded", "title": "Rate limit exceeded", "detail": "Too many requests in a given amount of time, please try again in one hour." } ``` _Learn more about retries [over here](/openapi/responses/retries)._ ## Rate limiting headers Rate limiting headers are HTTP headers that provide information about the rate limit status of the API. These headers can be included in the success response **unlike** `Retry-After` which goes on the error. They inform the client about their current rate limit status, including the number of requests remaining, the time until the rate limit resets, and the total number of requests allowed. The following headers are commonly used for rate limiting, even if they are non-standard, but they show how to implement them in OpenAPI: ```yaml responses: "200": description: OK headers: X-RateLimit-Limit: description: The maximum number of requests that the client is allowed to make in a given time period. schema: type: integer example: 1000 X-RateLimit-Remaining: description: The number of requests remaining in the current rate limit window. schema: type: integer example: 500 X-RateLimit-Reset: description: The number of seconds until the rate limit resets. schema: type: integer example: 35 ``` This response describes a successful request with rate limiting headers included. ```http HTTP/2 200 OK X-RateLimit-Limit: 1000 X-RateLimit-Remaining: 500 X-RateLimit-Reset: 35 Content-Type: application/json { "data": { "id": 1, "name": "example" } } ``` ## Rate limiting headers draft RFC The `X-RateLimit-*` headers are not standard HTTP headers, and they are not defined in any RFC. A new [RateLimit header draft RFC](https://datatracker.ietf.org/doc/draft-ietf-httpapi-ratelimit-headers/), is in the works, but is still a draft and not yet an official standard. This draft RFC outlines a new `RateLimit` header to replace all of the `X-RateLimit-*` headers, and with a single header it can provide information about the rate limit status of the API, including the number of requests remaining, the time until the rate limit resets, and the total number of requests allowed. The following example shows a `RateLimit` header with a policy named "default", which has another 50 requests allowed in the next 30 seconds. ```http RateLimit: "default";r=50;t=30 ``` The `RateLimit` header focuses on the current state of the various quotes, but it doesn't provide information about the policy itself. The same draft RFC also outlines a `RateLimit-Policy` header which can be used to provide information about how the policy works. This example shows two policies, "default" and "daily". The "default" policy has a quota of 100 requests and a window of 30 seconds, while the "daily" policy has a quota of 1000 requests and a window of 86400 seconds (24 hours). ```http RateLimit-Policy: "default";q=100;w=30,"daily";q=1000;w=86400 ``` Trying to describe these headers in OpenAPI is a bit tricky, as the syntax moves beyond the simple `key: value` pairs that OpenAPI is used to. The following example shows how to describe the `RateLimit` header in OpenAPI, using a string with a `pattern` to validate the format of the header value. ```yaml headers: RateLimit: description: The rate limit status of the API. schema: type: string pattern: ^"[^"]+";r=\d+;t=\d+$ example: '"default";r=50;t=30' ``` This example uses a regular expression to validate the format of the `RateLimit` header value, which is a string that contains the policy name, the number of requests remaining, and the time until the rate limit resets. The `pattern` field is used to specify the regular expression that the header value must match. The `example` field is used to provide an example of the header value, which is a string that contains the policy name, the number of requests remaining, and the time until the rate limit resets. The `RateLimit-Policy` header can be described in a similar way, using a string with a `pattern` to validate the format of the header value. ```yaml headers: RateLimit-Policy: description: The rate limit policy of the API. schema: type: string pattern: ^"[^"]+";q=\d+;w=\d+$ example: '"default";q=100;w=30,"daily";q=1000;w=86400' ``` This example uses a regular expression to validate the format of the `RateLimit-Policy` header value, which is a string that contains the policy name, the quota of requests, and the window of time. The `pattern` field is used to specify the regular expression that the header value must match. The `example` field is used to provide an example of the header value, which is a string that contains the policy name, the quota of requests, and the window of time. If these headers are being used, it might be easiest to provide some simple examples in the operation definitions, and dedicate a more specific "Rate Limiting Guide" somewhere in the API documentation to explain it all more fully elsewhere. # Retries in OpenAPI Source: https://speakeasy.com/openapi/responses/retries Retries are a common pattern in API design, especially when dealing with transient errors or rate limits, or connection issues that could have just been a bit getting flipped somewhere in the ether, but a second attempt would likely work. API consumers may or may not automatically retry failed requests, and if they do, they may not know how long to wait before retrying. This is especially true for rate limits, where the API may be able to handle a burst of requests, but then throttle the consumer for a period of time to avoid overwhelming the system. To avoid confusing consumers who just want to work with an API, it's a good idea to communicate to them how retries should be used in the API, with that information clearly explained in API documentation and automatically handled by SDKs. Whilst OpenAPI does not having any dedicated functionality describing retries, it does not need to, as HTTP itself has retries covered. The `Retry-After` header is defined in RFC 9110 as the standard way to communicate that a client or server failure has happened, but that a retry is possible after a certain amount of time. ```yaml responses: "429": description: Too Many Requests headers: Retry-After: description: The number of seconds to wait before retrying the request. schema: type: integer example: 120 ``` The most common usage of Retry-After is using the number of seconds to wait before trying again, but it can also be used to communicate a date and time when the request can be retried. ```yaml responses: "429": description: Too Many Requests headers: Retry-After: description: The number of seconds to wait before retrying the request. schema: type: string format: date-time example: 2025-07-01T12:00:00Z ``` This header is used in a number of different scenarios, including: - 404 Not Found - The server MAY send a Retry-After header field to indicate that the resource is temporarily unavailable, or could exist in the near future. - 408 Request Timeout - The server MAY send a Retry-After header field to indicate that it is temporary and after what time the client MAY try again. - 409 Conflict - The server MAY send a Retry-After header field to indicate that the conflict could be temporary and suggest a time where it may have been resolved. - 413 Content Too Large - If the condition is temporary, the server could send a Retry-After header field to indicate that it is temporary and the client could try again. - 429 Too Many Requests - The client has sent too many requests in a given amount of time, and the server is asking the client to wait before sending more requests. - 503 Service Unavailable - A service may be unavailable temporarily, so a Retry-After header field could suggest an appropriate amount of time for the client to wait before checking if the service is back. Some other 5XX errors could well be retryable, such as 504 Gateway Timeout, and a 507 Insufficient Storage might resolve itself, but it's possible to go too far. For example, 501 Not Implemented could be seen as temporary because it could be implemented soon, but nobody wants API consumers retrying to API forever waiting for a feature to be deployed which is never going to be deployed. Describing every single instance of anything that could ever happen in OpenAPI is not the goal of any API documentation because that is impossible. Focusing on key areas where retries are likely to be needed is a good balance, and from there, API consumers can use their own judgement on whether to retry or not. ## Examples or constants When describing some of these headers it feels important to provide a bit of context about what the headers contain, either with an exact value, or if that's not possible then some examples of what values might appear. Here is an example of how to do that with the `Retry-After` header, using the Header Object `example` field: ```yaml headers: Retry-After: description: The number of seconds to wait before making another request. schema: type: integer example: 3600 ``` Using an example here is important because the value of `Retry-After` can vary depending on the billing plan the user is on. For example, a free plan might have a `Retry-After` value of 60 seconds, while a paid plan might have a `Retry-After` value of 10 seconds. If the value of `Retry-After` is always the same, then using `const` inside the `schema` object is a good way to indicate that (available from OpenAPI v3.1). ```yaml headers: Retry-After: description: The number of seconds to wait before making another request. schema: type: integer const: 3600 ``` This indicates that the `Retry-After` value is always 3600 seconds, regardless of the billing plan the user is on. ## x-speakeasy-retries Another ways to communicate how retries should work, is using the Speakeasy vendor extension `x-speakeasy-retries`. Using this extension, OpenAPI can communicate retry for a particular operation, or the API as a whole, to API consumers. This extension can be used to specify the number of retries, the delay between retries, and the maximum delay. ```yaml /webhooks/subscribe: post: operationId: subscribeToWebhooks servers: - url: https://speakeasy.bar x-speakeasy-usage-example: tags: - server - retries x-speakeasy-retries: strategy: backoff backoff: initialInterval: 10 maxInterval: 200 maxElapsedTime: 1000 exponent: 1.15 ``` This example shows how to use the `x-speakeasy-retries` extension to specify a "backoff" strategy for retries, which is a common pattern for retrying failed requests with longer waits between retries. The `initialInterval` is the time to wait before the first retry, and the `maxInterval` is the maximum time to wait between retries. The `maxElapsedTime` is the maximum time to wait before giving up on retries, and the `exponent` is the exponent to use for the backoff calculation. This approach could be used as well as, or instead of, the `Retry-After` header. It would be a good way to add automatic retries to SDKs which means the client will be automatically retrying things without needing to learn everything about which status codes are retryable, and how long to wait before retrying. ## Rate Limits Rate limits are a common use case for retries, and the `Retry-After` header is a great way to communicate to API consumers when they should retry requests, and how long to wait before retrying. Learn more about [rate limiting with OpenAPI](/openapi/responses/rate-limiting). ## Best practices ### Examples over constants When describing the `Retry-After` header, it's a good idea to use examples instead of constants to allow for flexibility in the API. Some APIs will push up the `Retry-After` value during times of high load, and some APIs will have different `Retry-After` values depending on the billing plan the user is on. Using examples allows for this flexibility, whilst still letting API consumers know what sort of values they can expect (e.g: integer vs date-time). ### Expand Retry-After in periods of high load When the API is under heavy load, it's a good idea to expand the `Retry-After` header to allow for longer wait times between retries. This can help reduce the number of requests being made to the API, and can also help consumers avoid tripping over on rate limits. ### Use exponential backoff When retrying requests, it's a good idea to suggest an exponential backoff approach, as a server which is struggling to keep up with requests does not need to be hammered with even more requests. Sometimes it needs a moment to recover, and clients giving larger gaps between retries can help with that. ### Document alternative solutions Once the maximum number of retries has been reached, it's is likely to be an actual outage instead of simply a blip in availability. The API client and their end-users should be informed of this, and given alternative solutions to the problem. This could be a link to a status page, or a support email address. For example, at a coworking space, when the conference room booking system was down, the client would replace the "Submit Booking" button with a message saying "The booking system is down, please email somebody@example.org". OpenAPI is a great way to communicate this sort of information to API clients so they can use it to inform their users. ```yaml responses: "503": description: Service Unavailable headers: Retry-After: description: The number of seconds to wait before making another request. schema: type: integer example: 3600 content: application/problem+json: schema: type: object properties: type: type: string const: "https://example.com/probs/service-unavailable" title: type: string const: Service Unavailable detail: type: string const: The service is currently unavailable, please try again later, or contact support@example.org to report the issue. ``` # Data Types in OpenAPI Source: https://speakeasy.com/openapi/schemas import { Table } from "@/mdx/components"; The Schema Object represents any data type used as input or output in OpenAPI. The standard supports the following data types: - [Strings](/openapi/schemas/strings) - A sequence of characters. (dates, times, passwords, byte, and binary data are considered strings) - [Numbers](/openapi/schemas/numbers) - A number, either integer or floating-point. - [Booleans](/openapi/schemas/booleans) - A true or false value. - [Arrays](/openapi/schemas/arrays) - A collection of other data types. - [Objects](/openapi/schemas/objects) - A collection of key-value pairs. - [Enums](/openapi/schemas/enums) - A fixed list of possible values. - [Null](/openapi/schemas/null) - A null value. Schema objects are sometimes referred to as _models_, _data types_, or simply, _schemas_. This is because schema types are used to model complex data types used by an API. The Schema Object is based on and extends the [JSON Schema Specification Draft 2020-12](https://datatracker.ietf.org/doc/html/draft-bhutton-json-schema-00). OpenAPI 3.1 uses all vocabularies from JSON Schema 2020-12, except for Format Assertion. For an overview of all JSON Schema properties, see [JSON Schema Docs > JSON Schema 2020-12](https://www.learnjsonschema.com/2020-12/). OpenAPI 3.1 changes the definition of two JSON Schema properties: - `description` - In OpenAPI this property may contain [CommonMark syntax](https://spec.commonmark.org/) to provide a rich description. - `format` - OpenAPI extends JSON Schema data types by adding additional formats. See [Data Type Formats](/openapi/schemas). OpenAPI adds another vocabulary to JSON Schema with the following properties: <Table data={[ { fieldName: "`discriminator`", type: "[Discriminator Object](/openapi/schemas/objects/polymorphism#discriminator-object)", description: "A discriminator object describes how to differentiate between related schemas based on the value of a field in a request or response. See [Composition and Inheritance](/openapi/schemas/objects/polymorphism)." }, { fieldName: "`xml`", type: "[XML Object](/openapi/schemas/objects/xml)", description: "Adds details about how the schema should be represented as XML." }, { fieldName: "`externalDocs`", type: "[External Documentation Object](/openapi/external-documentation)", description: "Points to external documentation for this schema." }, { fieldName: "`example`", type: "Any", description: "An example that satisfies this schema. **Deprecated:** Although valid, the use of `example` is discouraged. Use [Examples](/openapi/examples) instead." }, { fieldName: "`x-`", type: "[Extensions](/openapi/extensions)", description: "Any number of extension fields can be added to the schema that can be used by tooling and vendors." }, { fieldName: "Arbitrary properties", type: "Any", description: "The schema object supports arbitrary properties without the `x-` prefix. This is discouraged in favor of [Extensions](/openapi/extensions)." } ]} columns={[ { key: "fieldName", header: "Field Name" }, { key: "type", header: "Type" }, { key: "description", header: "Description" } ]} /> The example below illustrates three schema objects: `IngredientProductCode`, `Ingredient`, and `IngredientType`. ```yaml components: schemas: IngredientProductCode: description: The product code of an ingredient, only available when authenticated. type: string examples: - "AC-A2DF3" - "NAC-3F2D1" - "APM-1F2D3" Ingredient: type: object properties: name: description: The name of the ingredient. type: string examples: - Sugar Syrup - Angostura Bitters - Orange Peel type: $ref: "#/components/schemas/IngredientType" stock: description: The number of units of the ingredient in stock, only available when authenticated. type: integer examples: - 10 - 5 - 0 readOnly: true productCode: $ref: "#/components/schemas/IngredientProductCode" photo: description: A photo of the ingredient. type: string format: uri examples: - https://speakeasy.bar/ingredients/sugar_syrup.jpg - https://speakeasy.bar/ingredients/angostura_bitters.jpg - https://speakeasy.bar/ingredients/orange_peel.jpg required: - name - type IngredientType: description: The type of ingredient. type: string enum: - fresh - long-life - packaged ``` ## JSON Schema and OpenAPI OpenAPI 3.0 was not totally compatible with JSON schema. That caused, and continues to cause, issues in tooling support. Fortunately, OpenAPI 3.1 is now a superset of JSON Schema, meaning compatibility with any valid JSon Schema document. # null in OpenAPI Source: https://speakeasy.com/openapi/schemas/null ## OpenAPI 3.0.X OpenAPI 3.0.X doesn't support a `null` type but instead allows you to mark a schema as being `nullable`. This allows that type to either contain a valid value or null. ```yaml # A nullable string schema: type: string nullable: true # A nullable integer schema: type: integer format: int32 nullable: true # A nullable boolean schema: type: boolean nullable: true # A nullable array schema: type: array items: type: string nullable: true # A nullable object schema: type: object properties: foo: type: string nullable: true ``` ## OpenAPI v3.1 OpenAPI v3.1 aligned describing `null` with JSON Schema. This allows for more precise API definitions, especially for APIs that need to explicitly support null values as valid inputs or outputs. To specify that a property, item, or response can be `null`, you can use the `type` keyword with a value of `null` or combine null with other types using the `oneOf` or type array syntax. This flexibility makes it easier to accurately model your data. ```yaml # A nullable string using array syntax schema: type: [ 'null', 'string' ] # A nullable field using an array schema: type: object properties: foo: type: ['null', 'string'] # A nullable field using oneOf schema: type: object properties: foo: oneOf: - type: 'null' - type: string ``` # numbers and integers in OpenAPI Source: https://speakeasy.com/openapi/schemas/numbers The **number/integer** types allows the describing of various number formats through a combination of the **type** and **format** attribute, along with a number of attributes for validating the data, the spec should cover most use cases. Available formats are: | Type | Format | Explanation | Example | | --- | --- | --- | --- | | number | | Any number integer/float at any precision. | **10** or **1.9** or **9223372036854775807** | | number | float | 32-bit floating point number. | **1.9** | | number | double | 64-bit floating point number. | **1.7976931348623157** | | integer | | Any integer number. | **2147483647** or **9223372036854775807** | | integer | int32 | 32-bit integer. | **2147483647** | | integer | int64 | 64-bit integer. | 9223372036854775807 | Below are some examples of defining **number/integer** types: ```yaml # Any number schema: type: number # A 32-bit floating point number schema: type: number format: float # A 64-bit floating point number schema: type: number format: double # Any integer schema: type: integer # A 32-bit integer schema: type: integer format: int32 # A 64-bit integer schema: type: integer format: int64 ``` Various tools may treat a **number/integer** without a format attribute as a type capable of holding the closest representation of that number in the target language. For example, a **number** might be represented by a **double,** and an **integer** by an **int64.** Therefore, it's recommended that you **be explicit with the format of your number type and always populate the format attribute**. The **number** type also has some optional attributes for additional validation: - **minimum** \- The **minimum** inclusive number the value should contain. - **maximum** \- The **maximum** inclusive number the value should contain. - **exclusiveMinimum** \- Make the **minimum** number exclusive. - **exclusiveMaximum** \- Make the **maximum** number exclusive. - **multipleOf** \- Specify the **number/integer** is a multiple of the provided value. Some examples are below: ```yaml # An integer with a minimum inclusive value of 0 schema: type: integer format: int32 minimum: 10 # An integer with a minimum exclusive value of 0 schema: type: integer format: int32 minimum: 0 exclusiveMinimum: true # A float with a range between 0 and 1 schema: type: number format: float minimum: 0 maximum: 1 # A double with an exclusive maximum of 100 schema: type: number format: double maximum: 100 exclusiveMaximum: true # An 64 but integer that must be a multiple of 5 schema: type: integer format: int64 multipleOf: 5 ``` # oneOf, allOf, anyOf: composition and inheritance in OpenAPI Source: https://speakeasy.com/openapi/schemas/objects/polymorphism import { Table } from "@/mdx/components"; OpenAPI allows us to combine object schemas using the keywords `allOf`, `anyOf`, and `oneOf`. These keywords correspond to the following logical operators: <Table data={[ { keyword: "`oneOf`", operator: "`XOR`", description: "An exclusive disjunction. Instances must satisfy **exactly one of** A, B, or C.", howToUse: "Use for describing Union Types" }, { keyword: "`allOf`", operator: "`AND`", description: "A union of all subschemas. Instances must satisfy **all of** A, B, and C.", howToUse: "Use for describing model composition: the creation of complex schemas via the composition of simpler schemas." }, { keyword: "`anyOf`", operator: "`OR`", description: "An inclusive disjunction. Instances must satisfy **at least one of** A, B, or C.", howToUse: "There is no established convention about how anyOf should be interpreted. **Use with extreme caution**" } ]} columns={[ { key: "keyword", header: "Keyword" }, { key: "operator", header: "Operator" }, { key: "description", header: "Description" }, { key: "howToUse", header: "How to use" } ]} /> The example below illustrates the different composition keywords: ```yaml components: schemas: # ... Other schemas ... Negroni: description: A Negroni cocktail. Contains gin, vermouth and campari. allOf: - $ref: "#/components/schemas/Vermouth" - $ref: "#/components/schemas/Gin" - $ref: "#/components/schemas/Campari" Martini: description: A Martini cocktail. Contains gin and vermouth, or vodka and vermouth. oneOf: - $ref: "#/components/schemas/Vodka" - $ref: "#/components/schemas/Gin" - $ref: "#/components/schemas/Vermouth" Punch: description: A Punch cocktail. Contains any combination of alcohol. anyOf: - $ref: "#/components/schemas/Rum" - $ref: "#/components/schemas/Brandy" - $ref: "#/components/schemas/Whisky" - $ref: "#/components/schemas/Vodka" - $ref: "#/components/schemas/Gin" ``` ## Discriminator Object in OpenAPI When using `oneOf` to indicate that a request body or response contains exactly one of multiple [Schema Objects](/openapi/schemas), a discriminator object can help the client or server figure out which schema is included in the request or response. The discriminator object in OpenAPI tells a client or server which field can be used to discriminate between different schemas. <Table data={[ { field: "`propertyName`", type: "String", required: "✅", description: "The property name used to discriminate between schemas." }, { field: "`mapping`", type: "Map[string, string]", required: "", description: "An optional map of values and schema reference strings." }, { field: "`x-*`", type: "[Extensions](/openapi/extensions)", required: "", description: "Any number of extension fields can be added to the discriminator object that can be used by tooling and vendors." } ]} columns={[ { key: "field", header: "Field" }, { key: "type", header: "Type" }, { key: "required", header: "Required" }, { key: "description", header: "Description" } ]} /> In the example below, the Speakeasy Bar can receive one of two order types: A drink order with a bar-counter reference or an ingredient order with a delivery address: ```yaml components: responses: OrderResponse: oneOf: - $ref: "#/components/schemas/DrinkOrder" - $ref: "#/components/schemas/IngredientOrder" ``` If we include a discriminator object, the client can indicate the order type so that the server does not need to figure that out: ```yaml components: responses: OrderResponse: oneOf: - $ref: "#/components/schemas/DrinkOrder" - $ref: "#/components/schemas/IngredientOrder" discriminator: propertyName: orderType ``` In the previous example, the value of the `orderType` property will determine the order type. The value of `orderType` must match one of the schema components, so must be either `DrinkOrder` or `IngredientOrder`. To use values that don't match a schema key, a discriminator object can include a `mapping` property that maps values to schemas. Here's an example: ```yaml components: responses: OrderResponse: oneOf: - $ref: "#/components/schemas/DrinkOrder" - $ref: "#/components/schemas/IngredientOrder" discriminator: propertyName: orderType mapping: drink: "#/components/schemas/DrinkOrder" ingredient: "#/components/schemas/IngredientOrder" ``` # XML Object in OpenAPI Source: https://speakeasy.com/openapi/schemas/objects/xml import { Table } from "@/mdx/components"; The XML Object allows us to add details about how the schema should be represented as XML. This is useful because XML has different data types and structures compared to JSON. For example, in JSON, an array is a list of values only, while in XML, array values are represented as elements with names. <Table data={[ { field: "`name`", type: "String", required: "", description: "The name of the element when the property is represented in XML. When used in `items`, the name applies to each element in the XML array." }, { field: "`namespace`", type: "String", required: "", description: "The absolute URL of the XML namespace." }, { field: "`prefix`", type: "String", required: "", description: "A prefix for the element's name." }, { field: "`attribute`", type: "Boolean", required: "", description: "Whether the property should be represented as an XML attribute (`<drink id=\"3\" />`) instead of an XML element (`<drink><id>3</id></drink>`). Defaults to `false`, so each property is represented as an element by default." }, { field: "`wrapped`", type: "Boolean", required: "", description: "Whether array elements should be wrapped in a container element. Defaults to `false`, so array elements are not wrapped by default. Only applies to arrays." }, { field: "`x-*`", type: "[Extensions](/openapi/extensions)", required: "", description: "Any number of extension fields can be added to the XML object that can be used by tooling and vendors." } ]} columns={[ { key: "field", header: "Field" }, { key: "type", header: "Type" }, { key: "required", header: "Required" }, { key: "description", header: "Description" } ]} /> The examples below illustrate how XML Objects can be used: ```yaml components: schemas: Drink: type: object properties: name: type: string xml: name: drinkName namespace: http://speakeasy.bar/schemas prefix: se ingredients: type: array items: $ref: "#/components/schemas/Ingredient" xml: name: ingredients wrapped: true namespace: http://speakeasy.bar/schemas prefix: se Ingredient: type: object properties: id: type: number xml: name: ingredientId namespace: http://speakeasy.bar/schemas prefix: se attribute: true name: type: string xml: name: ingredientName namespace: http://speakeasy.bar/schemas prefix: se ``` The example above translates to the following XML example: ```xml <se:drink xmlns:se="http://speakeasy.bar/schemas"> <se:drinkName>Mojito</se:drinkName> <se:ingredients> <se:ingredient se:id="1"> <se:ingredientName>Sugar</se:ingredientName> </se:ingredient> <se:ingredient se:id="2"> <se:ingredientName>Lime</se:ingredientName> </se:ingredient> <se:ingredient se:id="3"> <se:ingredientName>Mint</se:ingredientName> </se:ingredient> </se:ingredients> </se:drink> ``` # strings Source: https://speakeasy.com/openapi/schemas/strings import { Table } from "@/mdx/components"; The **string** type is one of the most used and most flexible primitive types in OpenAPI. It supports a number of formats, patterns and other validations that overlay constraints to the type of data represented. This is not just helpful for documentation and validation, but it can help with mapping to types in various languages when using OpenAPI for code generation. ## Formats The string type can contain anything, from passwords, IP addresses, email addresses, long form text, binary data, pretty much anything. To help describe the data in the string more specifically, OpenAPI supports a `format` keyword: ```yaml schema: type: object properties: createdAt: type: string format: date-time ``` Here are a few common formats that are likely to pop up. <Table title="Common String Formats" data={[ { type: "string", format: "date", explanation: "An [RFC3339](https://www.rfc-editor.org/rfc/rfc3339#section-5.6) formatted date string", example: '"2022-01-30"', }, { type: "string", format: "date-time", explanation: "An [RFC3339](https://www.rfc-editor.org/rfc/rfc3339#section-5.6) formatted date-time string", example: '"2019-10-12T07:20:50.52Z"', }, { type: "string", format: "password", explanation: "Provides a hint that the string may contain sensitive information.", example: '"mySecretWord1234"', }, { type: "string", format: "uuid", explanation: "A Universally Unique IDentifier as defined in RFC4122.", example: '"cde3dd4f-cb0e-47a1-8e2a-3595c0fa1cd1"', }, { type: "string", format: "byte", explanation: "Base-64 encoded data.", example: '"U3BlYWtlYXN5IG1ha2VzIHdvcmtpbmcgd2l0aCBBUElzIGZ1biE="', }, { type: "string", format: "binary", explanation: "Binary data, used to represent the contents of a file.", example: '"01010101110001"', }, ]} columns={[ { key: "type", header: "Type" }, { key: "format", header: "Format" }, { key: "explanation", header: "Explanation" }, { key: "example", header: "Example" }, ]} /> The format property has grown substantially over time, and a new [Format Registry](https://spec.openapis.org/registry/format/) has been defined which OpenAPI v3.1 and future versions defer to. - base64url - binary - byte - char - commonmark - date-time - date - decimal - decimal128 - double - duration - email - float - hostname - html - http-date - idn-email - idn-hostname - int16 - int32 - int64 - int8 - ipv4 - ipv6 - iri-reference - iri - json-pointer - media-range - password - regex - relative-json-pointer - sf-binary - sf-boolean - sf-decimal - sf-integer - sf-string - sf-token - time - uint8 - uri-reference - uri-template - uri - uuid This list is huge already and likely to grow over time. Not all tooling will understand every single one, but that's ok because format is an extensible property in OpenAPI: anyone could put any value in there, and if a tool knows what it means, it can do something with it. ### Examples ```yaml schema: properties: # A unique id uuid: type: string format: uuid # A basic string basicString: type: string # A string that represents a RFC3339 formatted date-time string createdAt: type: string format: date-time # A string that represents a enum with the specified values status: type: string enum: - "pending" - "approved" - "archived" ``` ## Patterns The **string** type also has an associated `pattern` keyword which accepts a regular expression, which can help further define a string when no particular format is exactly appropriate. The regular expression syntax is the one defined in JavaScript ([ECMA 262](https://tc39.es/ecma262/) specifically) with Unicode support. The `pattern` keyword is part of the [JSON Schema](https://json-schema.org/) specification, which OpenAPI v3.1 extends for its `schema` keyword, so you can read more about [Regular Expressions](https://json-schema.org/understanding-json-schema/reference/regular_expressions) on the JSON Schema website. ### Examples Example of a string defined with a regex pattern: ```yaml schema: properties: username: type: string pattern: ^[a-zA-Z0-9_]*$ examples: - Fred - some_user - 123abc phoneNumber: type: string pattern: ^(\\([0-9]{3}\\))?[0-9]{3}-[0-9]{4}$ ``` ## When to use pattern or format Pattern is used primarily for validation, but format is sometimes only treated as an informative annotation. In some cases it might be a good idea to provide a pattern as well as a format, just to make sure validation is run as expected. # Security in OpenAPI Source: https://speakeasy.com/openapi/security import { Table } from "@/mdx/components"; When designing an API, it's important to consider the security requirements for accessing the API. The OpenAPI Specification 3.1 provides a way to define security requirements at both the document and operation levels. Firstly, "security" is a general term that covers multiple authentication and authorization systems commonly used to ensure API operations are used only by the intended actors. OpenAPI divides security into two parts: - **Security schemes**, which are defined as reusable components. - **Security requirements**, which invoke the reusable security schemes either across the whole API or within specific operations. The following simple example of an API key demonstrates the basic security structure defined in the OpenAPI Specification: ```yaml security: - apiKey: [] components: securitySchemes: apiKey: type: apiKey name: Speakeasy-API-Key in: header ``` ## Supported security schemes Before you can reference a [security scheme](/openapi/security/security-schemes) as a requirement in the `security` section, it must be defined in the [Components Object](/openapi/components) under `securitySchemes`. The OpenAPI Specification 3.1 supports the following security schemes: - [API Key](/openapi/security/security-schemes/security-api-key) - [HTTP Authorization](/openapi/security/security-schemes/security-http) (such as Basic, Digest, and Bearer) - [OAuth 2.0](/openapi/security/security-schemes/security-oauth2) - [OpenID Connect](/openapi/security/security-schemes/security-openid) - [Mutual TLS](/openapi/security/security-schemes/security-mutualtls) Once you've defined an API's security schemes, you can reference them in the `security` section of the OpenAPI document or at the operation level. ## The Security Requirement Object A Security Requirement Object defines a map of security scheme names with an array of [scopes or roles](#security-requirement-scopes-or-roles) that are required to access an API (or specific operation). The names **must** match the names of the [Security Scheme Objects](/openapi/security/security-schemes) defined in the [Components Object](/openapi/components) under the `securitySchemes` field. <Table data={[ { field: "{securitySchemeName}", type: "List<string>", required: "", description: "A list of [scopes or roles](#security-requirement-scopes-or-roles) required for the security scheme. If the security scheme type is `oauth2` or `openIdConnect`.", }, ]} columns={[ { key: "field", header: "Field" }, { key: "type", header: "Type" }, { key: "required", header: "Required" }, { key: "description", header: "Description" }, ]} /> ## Global authentication vs endpoint authentication Security can be applied at two levels in OpenAPI: - **Global security:** The security specified is available for all operations. - **Per-operation security:** The security is only applied to the operation, overriding any global-level security. The following example demonstrates how to describe security at both levels: ```yaml security: - apiKey: [] # Global security requirement paths: /drinks: get: operationId: listDrinks summary: Get a list of drinks post: operationId: createDrink summary: Create a new drink security: - apiKey: [] # Per operation security requirement ``` The [`security`](https://spec.openapis.org/oas/v3.1.0#security-requirement-object) and [`securitySchemes`](https://spec.openapis.org/oas/v3.1.0#security-scheme-object/security-schemes) sections (representing the Security Object and Security Scheme Object, respectively) are most important in this example. ## Working with security requirements Depending on the API, security requirements can be simple or complex. The following examples illustrate how to work with security requirements in OpenAPI. ### Simple API security All the operations in an API may require an API key for access. You can define this requirement at the document level using the `security` section. ```yaml security: - apiKey: [] paths: /drinks: get: operationId: listDrinks summary: Get a list of drinks post: operationId: createDrink summary: Create a new drink components: securitySchemes: apiKey: type: apiKey name: Speakeasy-API-Key in: header ``` ### Public reads, protected writes A simple API might have some publicly accessible endpoints, but require an API key for others. You can use the `security` section to define the security requirements either for the entire API or for specific operations. ```yaml security: [] # No global security requirements paths: /drinks: get: operationId: listDrinks summary: Get a list of drinks post: operationId: createDrink summary: Create a new drink security: - apiKey: [] # This operation requires an API key ``` ### Multiple security schemes A more complex API may have different security requirements for different operations, or even allow multiple security schemes to be used interchangeably. In the following example, the API can be accessed with either an API key or OAuth 2.0. OAuth 2.0 allows for more granular access control using the concept of scopes, which can be defined in the `securitySchemes` section and referenced in the `security` section for specific operations. ```yaml # focus(3:16) paths: /drinks: get: operationId: listDrinks summary: Get a list of drinks security: - apiKey: [] # Can be access with an API key, or... - oauth2: # an oauth2 token which has the read scope - read post: operationId: createDrink summary: Create a new drink security: - apiKey: [] # Can be accessed with an API key, or... - oauth2: # an oauth2 token which has the write scope - write components: securitySchemes: apiKey: type: apiKey name: Speakeasy-API-Key in: header oauth2: type: oauth2 flows: authorizationCode: authorizationUrl: https://example.com/oauth/authorize tokenUrl: https://example.com/oauth/token scopes: read: Read access to the API write: Write access to the API ``` ## Security requirement scopes or roles in OpenAPI When defining an OAuth 2.0 or OpenID Connect [Security Requirement Object](/openapi/security#security-requirement-object) for an operation, the `{securitySchemeName}` field should contain a list of the scopes or roles required for the security scheme. For example, the following Security Requirement Object indicates that the `read` and `write` scopes are required for the `oauth2` security scheme: ```yaml paths: /drinks: get: operationId: listDrinks summary: Get a list of drinks # Operation requires read and write scopes security: - oauth2: - read - write # ... ``` ### Disabling security requirements for specific operations You can disable security for a specific operation by providing an empty array (`[]`) in the list of security requirements. In this example, the `POST` operation in the `/auth` path does not require security, despite the global security requirement of an API key: ```yaml security: - apiKey: [] paths: /auth: post: operationId: authenticate summary: Authenticate with the API security: [] # Disable security for this operation # ... ``` ### Optional security You can make security optional by providing an empty object (`{}`) in the list of security requirements. In this example, the API may be accessed with or without an API key: ```yaml security: - apiKey: [] - {} ``` ### Optional security for specific operations Similarly, you can make security optional for a specific operation by providing an empty object (`{}`) in the list of security requirements. This does not disable the security requirements defined at the document level, but makes them optional for this specific operation. In this example, the `GET` operation in the `/drinks` path may be accessed with or without an API key, but if authenticated, the response will include additional information: ```yaml paths: /drinks: get: operationId: listDrinks summary: Get a list of drinks. If authenticated, this will include stock levels and product codes, otherwise it will only include public information. security: - {} # Make security optional for this operation # ... ``` ### Requiring multiple security schemes If multiple schemes are required together, then the [Security Requirement Object](/openapi/security#security-requirement-object) should be defined with multiple security schemes. In this example, both an API key **AND** basic auth are required to access the API: ```yaml security: # both apiKey AND basic are required - apiKey: [] basic: [] ``` ### Complex authorization scenarios You can use this **AND**/**OR** logic, along with optional (`{}`) security, in any combination to express complex authorization scenarios. In this example, the API may be accessed with an API key **AND** OAuth 2.0, **OR** with basic authentication: ```yaml security: # apiKey AND oauth2 OR basic - apiKey: [] oauth2: - read - write - basic: [] ``` # The API Key security scheme in OpenAPI Source: https://speakeasy.com/openapi/security/security-schemes/security-api-key import { Table } from "@/mdx/components"; An API Key security scheme is the most common form of authentication for machine-to-machine APIs. It supports passing a pre-shared secret via the Authorization header (or another custom header), via a cookie, or as a query parameter. The following examples demonstrate the three methods for passing an API key. - As a query string: ```http GET /drinks?api_key=abcdef12345 ``` - As a request header: ```http GET /something HTTP/1.1 Speakeasy-API-Key: abcdef12345 ``` - As a cookie: ```http GET /something HTTP/1.1 Cookie: X-API-KEY=abcdef12345 ``` Although the API Key security scheme is one of the most commonly used mechanisms, its security depends on the type of key used and how it was generated. Passing an API key via a header or cookie is often more secure than passing it via a query parameter, because logging mechanisms often store query param information. The biggest security flaw is that most preshared secrets are long-lived, and if intercepted, can be used until they expire (generally in months or years) or are revoked. This risk is usually tolerated for machine-to-machine applications, as the chance of interception (especially when using private VPCs/TLS and other mechanisms) is relatively low compared to that of a key from a user's device traveling on a public network. Regardless of which method you use, using HTTPS or TLS is highly recommended. ## Defining the API Key security scheme The fields for an API Key security scheme are as follows: <Table data={[ { field: "`type`", type: "String", required: "✅", description: "`apiKey`" }, { field: "`description`", type: "String", required: "", description: "Human-readable information. This may contain [CommonMark syntax](https://spec.commonmark.org/) to provide a rich description." }, { field: "`in`", type: "String", required: "✅", description: "The location of the API key in the request. Valid values are `query`, `header`, or `cookie`." }, { field: "`name`", type: "String", required: "✅", description: "The name of the key parameter in the location." }, { field: "`x-*`", type: "[Extensions](/openapi/extensions)", required: "", description: "Any number of extension fields can be added to the Security Scheme Object to be used by tooling and vendors." } ]} columns={[ { key: "field", header: "Field" }, { key: "type", header: "Type" }, { key: "required", header: "Required" }, { key: "description", header: "Description" } ]} /> To describe an API key, first create an object in `securitySchemes` with a name of your choosing. You can use `ApiKey` or even `anythingYOULikeAuth`, as long as it's easy to remember and a valid YAML string. Then, define the object `type` as `apiKey` and define the name of the header, query, or cookie parameter as it should appear in the HTTP request: ```yaml components: securitySchemes: ApiKey: type: apiKey name: Speakeasy-API-Key in: header security: - ApiKey: [] ``` You can add a description to let API users know where to find an API key or which email address to contact to request one. ## Using multiple API keys Some APIs require two API keys to be used at once. For example, users may need to use an app ID and an app key simultaneously. In such cases, define both keys as `securitySchemes`, then reference them both in the same [Security Requirement Object](/openapi/security). ```yaml security: - AppId: [] AppKey: [] # no leading dash components: securitySchemes: AppId: type: apiKey name: Speakeasy-App-Id in: header AppKey: type: apiKey name: Speakeasy-App-Key in: header ``` # The HTTP security scheme in OpenAPI Source: https://speakeasy.com/openapi/security/security-schemes/security-http import { Table } from "@/mdx/components"; The `http` security scheme is one of the most commonly used schemes because it covers anything using the HTTP header `Authorization`. This is a long list, including auth schemes like HTTP Basic, HTTP Digest, and HTTP Bearer. The full list of supported auth schemes for `type: http` is outsourced to the [IANA HTTP Authentication Scheme Registry](https://www.iana.org/assignments/http-authschemes/http-authschemes.xhtml). Of that list, the auth schemes most likely to be used for a modern API are: - [Bearer](#http-bearer) - [Basic](#http-basic) - [Digest](#http-digest) ## HTTP Bearer The Bearer auth scheme allows you to pass a token, which could be a JSON Web Token (JWT), API token, access token, or other token-based form of authentication. ```http GET /secrets Authorization: Bearer <token> ``` Bearer is generally used for short-lived tokens granted to your API users through an additional login mechanism. Using a JWT allows for storing additional metadata within the token, which can be helpful for some use cases, such as storing scopes for permissions models. The fields for a Bearer auth scheme are as follows: <Table data={[ { field: "`type`", type: "String", required: "✅", description: "`http`" }, { field: "`description`", type: "String", required: "", description: "Human-readable information. This may contain [CommonMark syntax](https://spec.commonmark.org/) to provide a rich description.", }, { field: "`scheme`", type: "String", required: "✅", description: "`bearer`", }, { field: "`bearerFormat`", type: "String", required: "", description: "A hint to the client to identify how the bearer token is formatted. Bearer tokens are usually generated by an authorization server, so this information is primarily for documentation purposes.", }, { field: "`x-*`", type: "[Extensions](/openapi/extensions)", required: "", description: "Any number of extension fields can be added to the Security Scheme Object to be used by tooling and vendors.", }, ]} columns={[ { key: "field", header: "Field" }, { key: "type", header: "Type" }, { key: "required", header: "Required" }, { key: "description", header: "Description" }, ]} /> So, for example, a JWT might look like this: ```yaml components: securitySchemes: auth: type: http scheme: bearer bearerFormat: JWT security: - auth: [] ``` ## HTTP Basic The Basic auth scheme is a simple authentication mechanism baked into the HTTP protocol that supports sending an `Authorization` header containing a Base64-encoded username and password. ```http GET /secrets Authorization: Basic dXNlcjpwYXNzd29yZA== ``` Basic is a relatively simple mechanism for getting started, but it risks leaking easy-to-decode passwords if used incorrectly. The example above is simply a `base64("user:password")` but it can fool some developers into thinking the password is actually encrypted or secret. Much like API keys, another issue with Basic is that it generally works with long-lived credentials, and if intercepted, these can be used by malicious actors until the credentials expire or are revoked. For these reasons it is generally best to avoid HTTP Basic. Since all APIs need to be accurately described (warts and all), here's how to describe a Basic auth scheme in OpenAPI: <Table data={[ { field: "`type`", type: "String", required: "✅", description: "`http`" }, { field: "`description`", type: "String", required: "", description: "Human-readable information. This may contain [CommonMark syntax](https://spec.commonmark.org/) to provide a rich description.", }, { field: "`scheme`", type: "String", required: "✅", description: "`basic`", }, { field: "`x-*`", type: "[Extensions](/openapi/extensions)", required: "", description: "Any number of extension fields can be added to the Security Scheme Object to be used by tooling and vendors.", }, ]} columns={[ { key: "field", header: "Field" }, { key: "type", header: "Type" }, { key: "required", header: "Required" }, { key: "description", header: "Description" } ]} /> For example: ```yaml components: securitySchemes: auth: type: http scheme: basic security: - auth: [] ``` The description field can be used to warn developers about the risks of using Basic authentication, and to encourage them to use more secure alternatives like Bearer tokens or OAuth 2.0. ```yaml components: securitySchemes: auth: type: http scheme: basic description: | **Warning:** Basic authentication sends credentials in an easily decodable format, so this should only be used over HTTPS. Instead of using your password please only use a short lived access token which can be obtained from <https://example.com/tokens>. ``` ## HTTP Digest The Digest auth scheme is a more secure alternative to Basic authentication. Instead of sending the username and password in plain text, Digest uses a challenge-response mechanism that hashes credentials with a nonce provided by the server. This helps protect against replay attacks and credential interception. A typical Digest authentication request looks like this: ```http GET /secrets Authorization: Digest username="user", realm="example", nonce="dcd98b7102dd2f0e8b11d0f600bfb0c093", uri="/secrets", response="6629fae49393a05397450978507c4ef1", opaque="5ccc069c403ebaf9f0171e9517f40e41" ``` The fields for a Digest auth scheme in OpenAPI are as follows: <Table data={[ { field: "`type`", type: "String", required: "✅", description: "`http`" }, { field: "`description`", type: "String", required: "", description: "Human-readable information. This may contain [CommonMark syntax](https://spec.commonmark.org/) to provide a rich description.", }, { field: "`scheme`", type: "String", required: "✅", description: "`digest`", }, { field: "`x-*`", type: "[Extensions](/openapi/extensions)", required: "", description: "Any number of extension fields can be added to the Security Scheme Object to be used by tooling and vendors.", }, ]} columns={[ { key: "field", header: "Field" }, { key: "type", header: "Type" }, { key: "required", header: "Required" }, { key: "description", header: "Description" }, ]} /> For example: ```yaml components: securitySchemes: digestAuth: type: http scheme: digest security: - digestAuth: [] ``` ## Overlap with other security schemes The HTTP security scheme overlaps with a few other, more specific schemes. The [API Key](/openapi/security/security-schemes/security-api-key) security scheme is better suited to non-standard API keys using custom headers, like `Acme-API-Key`, whereas the HTTP security scheme is specifically designed for HTTP-based authentication methods using the `Authorization` header. The HTTP security scheme also has some overlap with the [OAuth 2.0](/openapi/security/security-schemes/security-oauth2) and [OpenID Connect](/openapi/security/security-schemes/security-openid) security schemes. These may use bearer tokens and the JWT format, but they are more complex and involve additional flows and endpoints. It's better to use the `oauth2` or `openidConnect` security scheme for an API using the OAuth 2.0 or OpenID Connect protocol than to try to cover it with the `http` security scheme. ### Unauthorized response As well as describing the security requirements, it also helps to describe what happens when those requirements are not met. The `401 Unauthorized` response can be returned for requests with missing security credentials, showing what the client can expect to see if this happens. This response includes the `WWW-Authenticate` header, which you may want to mention. As with other generic responses, the `401` response can be defined as a component in the `responses` section, and referenced with `$ref` to avoid repetition. ```yaml paths: /drinks: get: # ... responses: # ... '401': $ref: '#/components/responses/HttpErrorUnauthorized' post: # ... responses: # ... '401': $ref: '#/components/responses/HttpErrorUnauthorized' components: responses: HttpErrorUnauthorized: description: Unauthorized headers: WWW_Authenticate: schema: type: string content: application/problem+json: schema: $ref: '#/components/schemas/ProblemDetails' ``` Learn more about describing [responses](/openapi/responses). ## Forbidden response The `403 Forbidden` response can be returned for requests that are authenticated but not authorized to access the resource. This response can also include the `WWW-Authenticate` header, but it is not required. As with the `401` response, the `403` response can be defined as a component in the `responses` section, and referenced with `$ref` to avoid repetition. This error uses the HTTP Problem Details format, which is a standard means of providing more information about the error. ```yaml paths: /drinks: get: # ... responses: # ... '403': $ref: '#/components/responses/HttpErrorForbidden' post: # ... responses: # ... '403': $ref: '#/components/responses/HttpErrorForbidden' components: responses: HttpErrorForbidden: description: Forbidden headers: WWW_Authenticate: type: string content: application/problem+json: schema: $ref: '#/components/schemas/ProblemDetails' ``` # The Mutual TLS security scheme in OpenAPI Source: https://speakeasy.com/openapi/security/security-schemes/security-mutualtls Mutual TLS (mTLS) is a security protocol that enhances the security of API communication by requiring both the client and server to authenticate each other using digital certificates. This two-way authentication ensures that only trusted parties can establish a connection, providing an additional layer of security. OpenAPI lets you define a Mutual TLS security scheme using the `mutualTLS` type. ## Defining a Mutual TLS security scheme Define a Mutual TLS security scheme in OpenAPI using the following structure: ```yaml components: securitySchemes: MutualTLS: type: mutualTLS description: Mutual TLS authentication for secure API communication. ``` The `mutualTLS` type requires no additional fields, as its primary purpose is to indicate that the API requires mutual TLS authentication. However, you can use the `description` field to provide API users with additional information about how to obtain a certificate. ```yaml components: securitySchemes: MutualTLS: type: mutualTLS description: > To access this API, you must provide a valid client certificate. Please submit a request to the [infrastructure team](https://example.com) with full information about what this application is to obtain a certificate. ``` Learn more about mutual TLS in the [OpenAPI Specification](https://spec.openapis.org/oas/v3.1.0#mutual-tls-security-scheme) or the [Cloudflare Learning Center](https://www.cloudflare.com/learning/access-management/what-is-mutual-tls/). # The OAuth 2 security scheme in OpenAPI Source: https://speakeasy.com/openapi/security/security-schemes/security-oauth2 import { Table } from "@/mdx/components"; OAuth 2 is a popular open authentication mechanism that supports an authentication flow allowing servers to authenticate on behalf of a user or an entire application. While more generally used for authenticating end users (for example, logging the user in with Facebook), OAuth 2 is also used for machine-to-machine flows where a whole application authenticates itself with the API (for example, connecting Shopify to Xero). OAuth 2 is considered more secure than other mechanisms due to its granting privileges through short-lived tokens that limit damage from intercepted tokens. Tokens that can only be used for a day can only cause damage for a day. A well-built API won't allow short-lived tokens to be used to escalate privileges (for example, to change a user's password), so less damage can be done while the token is valid. The OAuth 2 protocol defines multiple ways of building a request against the `tokenUrl` endpoint and supports multiple flows at once, so that developers can set up different types of integration for the same API. ## Defining the OAuth 2 security scheme The fields for an OAuth 2 security scheme are as follows: <Table data={[ { field: "`type`", type: "String", required: "✅", description: "`oauth2`" }, { field: "`description`", type: "String", required: "", description: "Human-readable information. This may contain [CommonMark syntax](https://spec.commonmark.org/) to provide a rich description." }, { field: "`flows`", type: "Map[{key}, [OAuth Flow Object](#oauth-flow-object)]", required: "✅", description: "An object containing configuration for the available OAuth 2 flows. Valid keys are `implicit`, `password`, `clientCredentials`, and `authorizationCode`." }, { field: "`x-*`", type: "[Extensions](/openapi/extensions)", required: "", description: "Any number of extension fields can be added to the Security Scheme Object to be used by tooling and vendors." } ]} columns={[ { key: "field", header: "Field" }, { key: "type", header: "Type" }, { key: "required", header: "Required" }, { key: "description", header: "Description" } ]} /> The following example shows an OAuth 2 security scheme using the `clientCredentials` flow: ```yaml components: securitySchemes: clientCredentials: type: oauth2 flows: clientCredentials: tokenUrl: https://speakeasy.bar/oauth2/token refreshUrl: https://speakeasy.bar/oauth2/refresh security: - clientCredentials: [] ``` ## Defining OAuth flows The value of the [OAuth Flows Object](https://spec.openapis.org/oas/v3.1.0#oauth-flows-object) is a map of [OAuth Flow Objects](https://spec.openapis.org/oas/v3.1.0#oauth-flow-object). The OpenAPI Specification 3.1 supports the following four OAuth Flow Objects: - The [Client Credentials](#the-client-credentials-flow) flow (using `clientCredentials`, previously `application` in OpenAPI 2.0) - The [Authorization Code](#the-authorization-code-flow) flow (using `authorizationCode`, previously `accessCode` in OpenAPI 2.0) - The [Password](#the-password-flow) flow (using `password`) - The [Implicit](#the-implicit-flow) flow (using `implicit`) Each OAuth Flow Object has its own configuration parameters, so let's look at them individually. ### The Client Credentials flow The Client Credentials flow is generally used for machine-to-machine communication that doesn't require a specific user's permission and context. Think of an entire application integrating with a whole other application, to sync billing information or other organization-wide information. <Table data={[ { field: "`tokenUrl`", type: "String", required: "✅", description: "The token URL to be used for this flow." }, { field: "`refreshUrl`", type: "String", required: "", description: "The URL to be used for refreshing the token. No refresh URL means the token is not refreshable." }, { field: "`scopes`", type: "Map[String, String]", required: "✅", description: "The available scopes for the OAuth 2 flow, with a description for each scope. Although the specification requires this field, it can be an empty object." }, { field: "`x-*`", type: "[Extensions](/openapi/extensions)", required: "", description: "Any number of extension fields can be added to an OAuth Flow Object to be used by tooling and vendors to add additional metadata and functionality to the OpenAPI Specification." } ]} columns={[ { key: "field", header: "Field" }, { key: "type", header: "Type" }, { key: "required", header: "Required" }, { key: "description", header: "Description" } ]} /> The example below shows an OAuth 2 security scheme using the `clientCredentials` flow: ```yaml components: securitySchemes: clientCredentials: type: oauth2 flows: clientCredentials: tokenUrl: https://speakeasy.bar/oauth2/token refreshUrl: https://speakeasy.bar/oauth2/refresh scopes: read: Grants read access write: Grants write access ``` ### The Authorization Code flow The Authorization Code flow is generally used for server-side applications that can safely store the client secret. <Table data={[ { field: "`authorizationUrl`", type: "String", required: "✅", description: "The authorization URL to be used for this flow." }, { field: "`tokenUrl`", type: "String", required: "✅", description: "The token URL to be used for this flow." }, { field: "`refreshUrl`", type: "String", required: "", description: "The URL to be used for refreshing the token. No refresh URL means the token is not refreshable." }, { field: "`scopes`", type: "Map[String, String]", required: "✅", description: "The available scopes for the OAuth 2 flow, with a description for each scope. Although the specification requires this field, it can be an empty object." }, { field: "`x-*`", type: "[Extensions](/openapi/extensions)", required: "", description: "Any number of extension fields can be added to an OAuth Flow Object to be used by tooling and vendors to add additional metadata and functionality to the OpenAPI Specification." } ]} columns={[ { key: "field", header: "Field" }, { key: "type", header: "Type" }, { key: "required", header: "Required" }, { key: "description", header: "Description" } ]} /> The following example shows an OAuth 2 security scheme using the `authorizationCode` flow: ```yaml components: securitySchemes: authorizationCode: type: oauth2 flows: authorizationCode: authorizationUrl: https://speakeasy.bar/oauth2/authorize tokenUrl: https://speakeasy.bar/oauth2/token refreshUrl: https://speakeasy.bar/oauth2/refresh scopes: read: Grants read access write: Grants write access ``` ### The Password flow The Password flow is generally used for trusted first-party clients that can securely store the client secret. <Table data={[ { field: "`tokenUrl`", type: "String", required: "✅", description: "The token URL to be used for this flow." }, { field: "`refreshUrl`", type: "String", required: "", description: "The URL to be used for refreshing the token. No refresh URL means the token is not refreshable." }, { field: "`scopes`", type: "Map[String, String]", required: "✅", description: "The available scopes for the OAuth 2 flow, with a description for each scope. Although the specification requires this field, it can be an empty object." }, { field: "`x-*`", type: "[Extensions](/openapi/extensions)", required: "", description: "Any number of extension fields can be added to an OAuth Flow Object to be used by tooling and vendors to add additional metadata and functionality to the OpenAPI Specification." } ]} columns={[ { key: "field", header: "Field" }, { key: "type", header: "Type" }, { key: "required", header: "Required" }, { key: "description", header: "Description" } ]} /> The following example shows an OAuth 2 security scheme using the `password` flow: ```yaml components: securitySchemes: password: type: oauth2 flows: password: tokenUrl: https://speakeasy.bar/oauth2/token refreshUrl: https://speakeasy.bar/oauth2/refresh scopes: read: Grants read access write: Grants write access ``` ### The Implicit flow The Implicit flow is generally used for browser or client-side applications that can't keep a client secret because, in a browser, all code and data are available to the user. Although it was popular for a while, the IETF's OAuth working group [recommends **not using the implicit grant** anymore](https://medium.com/oauth-2/why-you-should-stop-using-the-oauth-implicit-grant-2436ced1c926). <Table data={[ { field: "`authorizationUrl`", type: "String", required: "✅", description: "The authorization URL to be used for this flow." }, { field: "`refreshUrl`", type: "String", required: "", description: "The URL to be used for refreshing the token. No refresh URL means the token is not refreshable." }, { field: "`scopes`", type: "Map[String, String]", required: "✅", description: "The available scopes for the OAuth 2 flow, with a description for each scope. Although the specification requires this field, it can be an empty object." }, { field: "`x-*`", type: "[Extensions](/openapi/extensions)", required: "", description: "Any number of extension fields can be added to an OAuth Flow Object to be used by tooling and vendors to add additional metadata and functionality to the OpenAPI Specification." } ]} columns={[ { key: "field", header: "Field" }, { key: "type", header: "Type" }, { key: "required", header: "Required" }, { key: "description", header: "Description" } ]} /> The following example shows an OAuth 2 security scheme using the `implicit` flow: ```yaml components: securitySchemes: implicit: type: oauth2 flows: implicit: authorizationUrl: https://speakeasy.bar/oauth2/authorize refreshUrl: https://speakeasy.bar/oauth2/refresh scopes: read: Grants read access write: Grants write access ``` ## Using OAuth 2 with multiple flows If needed, you can use multiple OAuth 2 flows in a single API by describing each of the flows in the OAuth Flows Object. Consider the following example, which uses both the `authorizationCode` flow and the `clientCredentials` flow: ```yaml components: securitySchemes: oauth2: type: oauth2 flows: authorizationCode: authorizationUrl: https://speakeasy.bar/oauth2/authorize tokenUrl: https://speakeasy.bar/oauth2/token refreshUrl: https://speakeasy.bar/oauth2/refresh scopes: read: Grants read access write: Grants write access clientCredentials: tokenUrl: https://speakeasy.bar/oauth2/token refreshUrl: https://speakeasy.bar/oauth2/refresh scopes: read: Grants read access write: Grants write access security: - oauth2: [] ``` If you have an API in which different endpoints support different flows, split the endpoints into different `securitySchemes`. Consider this example, where the whole API is secured by `clientCredentials` except for the `/profile` endpoint, which requires `authorizationCode`: ```yaml security: - MachineAuth: [] paths: "/profile": security: - UserAuth: [] components: securitySchemes: UserAuth: type: oauth2 flows: authorizationCode: authorizationUrl: https://speakeasy.bar/oauth2/authorize tokenUrl: https://speakeasy.bar/oauth2/token refreshUrl: https://speakeasy.bar/oauth2/refresh scopes: read: Grants read access write: Grants write access MachineAuth: type: oauth2 flows: clientCredentials: tokenUrl: https://speakeasy.bar/oauth2/token refreshUrl: https://speakeasy.bar/oauth2/refresh scopes: read: Grants read access write: Grants write access ``` ## Scopes in OAuth 2 In an API, scopes define more granular permissions or access controls. APIs can use very generic scopes, like `read` and `write`. Other APIs focus on different resources: - `invoices` - `customers` - `orders` Some go even further and break scopes into resources and actions: - `invoices:read` - `invoices:write` - `invoices:delete` - `customers:read` - `customers:write` - `customers:delete` - `orders:read` - `orders:write` - `orders:delete` You can use detailed scopes not only to ensure that users don't see anything they shouldn't, but to limit accidental data loss when misconfigured applications go rogue. Scopes are defined in the `scopes` field of the OAuth Flow Object, and the scopes required for a specific operation are defined in the Security Requirement Object (the `security` field) for that operation: ```yaml components: securitySchemes: oauth2: type: oauth2 flows: authorizationCode: authorizationUrl: https://speakeasy.bar/oauth2/authorize tokenUrl: https://speakeasy.bar/oauth2/token refreshUrl: https://speakeasy.bar/oauth2/refresh scopes: 'drinks:read': Grants read access to drinks resource 'drinks:write': Grants write access to drinks resource paths: /drinks: get: operationId: listDrinks summary: Get a list of drinks security: - oauth2: - drinks:read ``` ### Using OAuth 2 without scopes Scopes are optional, and an API may not use them at all. To create an API without scopes, define `scopes` as an empty object `{}` and define the security requirement (for example, `oauth2` below) as an empty list of scopes `[]`. ```yaml components: securitySchemes: oauth2: type: oauth2 flows: authorizationCode: authorizationUrl: https://speakeasy.bar/oauth2/authorize tokenUrl: https://speakeasy.bar/oauth2/token refreshUrl: https://speakeasy.bar/oauth2/refresh scopes: {} security: - oauth2: [] ``` ## OAuth 2.1 and Device Authorization OAuth 2.1 is a simplified version of OAuth 2 that combines its best practices and removes the less secure flows. There aren't many changes directly related to OpenAPI, so the existing structure works fine for both versions. For more information on the changes and improvements made in OAuth 2.1, refer to [the OAuth 2.1 specification](https://datatracker.ietf.org/doc/html/draft-ietf-oauth-v2-1-13). **Device Authorization** is a new OAuth flow (also called a grant) that is not yet supported in the OpenAPI Specification 3.1. It will be added in the OpenAPI Specification 3.2, which is currently in development. For more information on the Device Authorization grant, take a look at the specification for the [OAuth 2.0 Device Authorization grant](https://datatracker.ietf.org/doc/html/rfc8628). # The OpenID Connect security scheme in OpenAPI Source: https://speakeasy.com/openapi/security/security-schemes/security-openid import { Table } from "@/mdx/components"; OpenID Connect is an authentication layer built on top of the OAuth 2.0 protocol. It allows clients to verify the identity of end-users based on the authentication performed by an authorization server, as well as to obtain basic profile information about the end-user in an interoperable and REST-like manner. As amazing as OAuth 2.0 is, it doesn't know much about a particular user. It recognizes a user as an entity that has an access token and has some conventions for identifying users, like using `GET /me` or inserting a user ID into a response. OpenID Connect standardizes these conventions and adds more convenience. The OpenAPI Specification supports OpenID Connect as a security scheme, allowing you to define the scopes and requirements for authentication in your API specification. This enables better security, a more consistent developer experience, and seamless integration with various OpenID Connect providers. ## Defining the OpenID Connect security scheme To define an OpenID Connect security scheme in your OpenAPI document, use the `openIdConnect` type in the `securitySchemes` section of the [Components Object](https://spec.openapis.org/oas/v3.1.0#components-object). The `openIdConnectUrl` field must point to a JSON OpenID Connect Discovery document, which provides metadata about the OpenID Connect provider. ```yaml components: securitySchemes: OpenIDAuth: type: openIdConnect openIdConnectUrl: https://example.com/.well-known/openid-configuration security: - OpenIDAuth: - drink:read - drink:write paths: /drinks: get: operationId: listDrinks summary: Get a list of drinks # Operation requires read scope security: - OpenIDAuth: - drink:read post: operationId: createDrink summary: Create a new drink # Operation requires write scope security: - OpenIDAuth: - drink:write ``` In OpenAPI, the OpenID Connect security scheme is defined as part of the [Security Scheme Object](https://spec.openapis.org/oas/v3.1.0#security-scheme-object). This allows you to specify the type of security scheme as `openIdConnect`, along with the URL to the OpenID Connect Discovery document. <Table data={[ { field: "`type`", type: "String", required: "✅", description: "`openIdConnect`" }, { field: "`description`", type: "String", required: "", description: "Human-readable information. This may contain [CommonMark syntax](https://spec.commonmark.org/) to provide a rich description." }, { field: "`openIdConnectUrl`", type: "String", required: "✅", description: "The URL must point to a JSON OpenID Connect Discovery document." }, { field: "`x-*`", type: "[Extensions](/openapi/extensions)", required: "", description: "Any number of extension fields can be added to the Security Scheme Object to be used by tooling and vendors." } ]} columns={[ { key: "field", header: "Field" }, { key: "type", header: "Type" }, { key: "required", header: "Required" }, { key: "description", header: "Description" } ]} /> ## Scopes in OpenID Connect Scopes limit the access granted to an application when it uses OpenID Connect. They define the permissions that the application can request from the user, such as reading or writing data. To use scopes in OpenID Connect, define them in the Security Scheme Object as an array of strings, where each string represents a specific permission that the application can request. ```yaml components: securitySchemes: OpenIDAuth: type: openIdConnect openIdConnectUrl: https://example.com/.well-known/openid-configuration description: "OpenID Connect security scheme with scopes for drink management." scopes: drink:read: "Allows reading drink information" drink:write: "Allows writing drink information" ``` To use these scopes in your API operations, also define them in the [Security Requirement Object](/openapi/security). This allows you to control which operations require which scopes, providing fine-grained access control at the global level as well as the operation level. ```yaml paths: /drinks: get: operationId: listDrinks summary: Get a list of drinks security: - OpenIDAuth: - drink:read post: operationId: createDrink summary: Create a new drink security: - OpenIDAuth: - drink:write ``` ## The benefits of using OpenID Connect Using OpenID Connect in your OpenAPI document provides advantages for SDK generation, documentation, and security, including the following key benefits: - **Standardization**: OpenID Connect provides a standardized way to handle user authentication and authorization, making it easier for developers to implement and understand. - **Interoperability**: OpenID Connect is widely supported by various identity providers, allowing seamless integration with existing systems. - **Security**: By defining scopes and security requirements in your OpenAPI document, you can enforce fine-grained access control, ensuring that only authorized users can access specific operations. - **Developer experience**: OpenAPI's support for OpenID Connect enhances the developer experience by providing clear documentation and guidelines for authentication, making it easier for developers to understand how to use your API securely. - **Tooling support**: Many tools and libraries support OpenID Connect, allowing for easy integration with your API and simplifying the authentication process for developers. # OpenAPI Servers Source: https://speakeasy.com/openapi/servers import { Table } from "@/mdx/components"; The servers section of OpenAPI is a straightforward yet powerful way to communicate the base URLs of API across different stages of the API lifecycle, or in different environments the end-users might be interested in. There may only be one API, but there is a development server, testing server, maybe a mocking server, or a sandbox for interacting with the API without real-world consequences, as well as the production API. People need to be able to find these servers and guessing is not a fun or productive solution. Here is an example of defining API servers in OpenAPI: ```yaml servers: - url: https://speakeasy.bar description: Production - url: https://sandbox.speakeasy.bar description: Sandbox - url: http://localhost:8088 description: Development x-internal: true ``` Servers can be defined at the [Document](/openapi#openapi-document-structure) level, the [Path](/openapi/paths) level, or the [Operation](/openapi/paths/operations) level, and since OpenAPI v3.0 can be a combination of domain names, subdirectories, ports, protocols, and even variables. This all gives flexibility in how everything "before the path" is defined, especially helping APIs that might have multiple domains, or dynamic domains. ## Server Object in OpenAPI A Server Object describes a single server that is available for the API. <Table data={[ { field: "`url`", type: "String", required: "✅", description: "A URL to the server. This can be an absolute URL or a relative URL to the hosted location of the OpenAPI document. The URL also supports variable substitutions via [Templating](/openapi/servers/server-variables)." }, { field: "`description`", type: "String", required: "", description: "A description of the server. [CommonMark syntax](https://spec.commonmark.org/) can be used to provide a rich description." }, { field: "`variables`", type: "[Server Variables](/openapi/servers/server-variables)", required: "", description: "A map of variable names to [Server Variable Objects](/openapi/servers/server-variables#server-variable-object) that can be used for variable substitution via [Templating](/openapi/servers/server-variables)." }, { field: "`x-*`", type: "[Extensions](/openapi/extensions)", required: "", description: "Any number of extension fields can be added to the Server Object ([for example, `x-speakeasy-server-id`](/docs/customize-sdks/servers#managing-multiple-servers-with-ids) that allows IDs to be assigned to each server for easier selection via Speakeasy SDKs) that can be used by tooling and vendors." } ]} columns={[ { key: "field", header: "Field" }, { key: "type", header: "Type" }, { key: "required", header: "Required" }, { key: "description", header: "Description" } ]} /> ## Absolute or relative The server URL can be a full URL or it could be a relative path. If the URL is an absolute path, it **_must_** conform to [RFC 3986](https://datatracker.ietf.org/doc/html/rfc3986) (`schema://host{:port}{/path}`) and not include the query string, and **_must_** be URL encoded (except for the templating delimiters `{}` if not part of the URL). The URL can also be a relative path to where the OpenAPI document is hosted (`/api`), at which point it will take the server from wherever the OpenAPI is hosted. For a document hosted at `https://speakeasy.bar/openapi.yaml`, the resulting URL will be `https://speakeasy.bar/api`. ## Path or operation servers If a list of servers is provided at the `paths` level, the servers will override any servers provided at the document level. If a list of servers is provided at the `operation` level, the servers will override any servers provided at the `paths` and document levels. ```yaml openapi: 3.1.0 servers: - url: https://speakeasy.bar description: Production paths: /drinks/{id}/image: servers: - url: https://uploads.speakeasy.bar description: Upload server put: summary: Upload an image for a drink parameters: - name: id in: path required: true description: The ID of the drink to upload an image for. schema: type: string requestBody: content: image/png: schema: type: string format: binary responses: "200": description: Image uploaded successfully. ``` In this example, the `servers` defined at the operation level will override the `servers` defined at the document level. The server URL for the operation will be `https://uploads.speakeasy.bar`, while the rest of the API will still use `https://speakeasy.bar`. Splitting out functionality like this can happen as [APIs evolve](/api-design/versioning), especially in this case of an [upload operation](/api-design/file-uploads) moving a dedicated endpoint to avoid spamming the main API server with large files. ## Server variables Server variables offer a convenient way to modify server URLs, covering simple patterns such as environment names, geographical regions, or covering wildcards like user-generated subdomains. These variables are part of the server object, and allow for more flexible API configurations without hardcoding every possible server option. For instance, consider an API that is deployed across multiple regions, such as the United States, Europe, and Asia. Instead of listing each server URL separately, use a server variable to represent the region. ```yaml servers: - url: "https://{region}.api.speakeasy.bar" description: Edge Server variables: region: default: us description: Please select the appropriate server. enum: - us - eu - asia ``` In this example, `{region}` is a server variable, and the `enum` restricts this to three possible values: `us`, `eu`, and `asia`. The default value is `us`, which means if the region is not specified, tooling can know which value to use. This setup allows clients to dynamically select the appropriate regional server by substituting the `{region}` variable in the URL template, resulting in `https://asia.api.example.com`. **Learn more about [server variables in OpenAPI](/openapi/servers/server-variables).** ## Best practices ### Best server first Depending on what tools are being used, servers can be used to do different things. For documentation the first server in the list is usually considered to be the default server to use, with logic to select other servers to use left up to tooling or the API consumer. ### Use x-internal to hide internal servers The `x-internal` attribute is not officially part of the specification, but it is a popular extension supported by many tools. Setting `x-internal: true` will hide these servers from public facing documentation for example. This allow them to be displayed for development and testing information in OpenAPI, but avoids confusing end-users with details about the internal setup. ```yaml servers: - url: http://localhost:8088 description: Development x-internal: true ``` # Server Variables in OpenAPI Source: https://speakeasy.com/openapi/servers/server-variables import { Table } from "@/mdx/components"; Most of the time an API uses a single server URL, like `https://api.speakeasy.bar`, and OpenAPI allows you to define this URL in the `servers` section. Even when an API is deployed to multiple production servers, they'll all be behind a load balancer which is using the same ingress URL so there's no need to get technical about individual servers. Sometimes things get a bit more complicated. For example, an API might be multi-tenant and deployed to multiple organizations based on custom data which is unknowable in the OpenAPI description, or deployed to multiple regions (United States, Europe, Asia). In these cases, where the server implementation is identical but it is not possible or necessary to define each one as its own server, a single server URL can be defined with Server Variables. These variables can even be replaced with the appropriate values for each organization in the API documentation, so that the documentation is more useful to consumers. For example, if an API is deployed to multiple organizations, the documentation can show the URL for each organization, making it easier for consumers to use the API. ## Server Variable Object Server variables are a map of variable names (string) to [Server Variable Objects](/openapi/servers/server-variables#server-variable-object) that can be used for variable substitution via Templating. A Server Variable Object describes a single variable that is optionally part of the URL in a [Server Object](/openapi/servers). The value of a variable can be any arbitrary string value unless a list of allowed values is provided via the `enum` field. <Table data={[ { field: "description", type: "String", required: "", description: "A description of the variable. [CommonMark syntax](https://spec.commonmark.org/) can be used to provide a rich description.", }, { field: "default", type: "String", required: "✅", description: "The default value of the variable. A variable is always of type _string_. If `enum` is provided this **_must_** be one of the values in the `enum` array.", }, { field: "enum", type: "List<string>", required: "", description: "A list of allowed string values for the variable.", }, { field: "x-*", type: "[Extensions](/openapi/extensions)", required: "", description: "Any number of extension fields can be added to the Server Variable Object that can be used by tooling and vendors.", }, ]} columns={[ { key: "field", header: "Field" }, { key: "type", header: "Type" }, { key: "required", header: "Required" }, { key: "description", header: "Description" }, ]} /> For example, the following OpenAPI document defines a server with a variable `organization` and `environment`: ```yaml servers: - url: https://{organization}.{environment}.speakeasy.bar description: A per-organization and per-environment API variables: organization: description: The organization name. Defaults to a generic organization. default: demo environment: description: The environment name. Defaults to the production environment. default: prod enum: - prod - staging - dev ``` Another interesting example is a server with a variable `port`: ```yaml servers: - url: http://localhost:{port} description: A local server variables: port: description: The port number. Defaults to 8080. default: 8080 enum: - 8080 - 8081 - 8082 ``` Some APIs might be deployed across multiple regions, such as the United States, Europe, and Asia. Instead of listing each server URL separately, use a server variable to represent the region. ```yaml servers: - url: "https://{region}.api.speakeasy.bar" description: Edge Server variables: region: default: us description: Please select the appropriate server. enum: - us - eu - asia ``` ## Templating syntax Any variable delimited by `{}` in the `url` field declares a part of the URL that **_must_** be replaced with a value and references a variable that **_must_** be defined in the `variables` map. It is the API consumer's responsibility to replace these variables (including the delimiters) with values to create a valid URL before making a request to the API. The defined `default` should be used if no other value is provided. ## Best practices ### Use meaningful names When naming server variables, use meaningful names that describe the purpose of the variable. Avoid using generic names like `var1`, `var2`, instead use meaningful names like `organization` and `region`. This will make it easier for consumers to understand the purpose of the variable and how to use it. ### Avoid using server variables for multiple API versions Some people try to use server variables for handling API Versions (v1, v2, v3) in a single OpenAPI document. This is a poor fit for server variables, because far more than the server URL will change between major versions. Server variables help when just the server is changing, but the other operations and components are the same. If the operations and components are the same, there was probably no need to create a new version of the API. If the operations and components are different, then you should create a new OpenAPI document for each version of the API. This is a much cleaner approach and will make it easier for consumers to understand the differences between versions. ### Beware enums When using enums, be careful to ensure that the values are valid and meaningful. Also make sure they are not too restrictive. For example, if you have a server variable for `region`, and you only allow `us`, `eu`, and `asia`, then it can be difficult to add new regions in the future. Old code would continue to validate against the predefined values, meaning any new regions would be rejected as invalid. Consider using a more generic approach, with valid regions explained in the description. This makes it easier to add new regions in the future without breaking existing code. ### Use default values When defining server variables, always provide a default value. This will make it easier for consumers to use the API without having to specify a value for every variable. The default value should be a reasonable value that is likely to be used by most consumers. # OpenAPI Tags Source: https://speakeasy.com/openapi/tags import { Table } from "@/mdx/components"; Tags are a way of grouping operations for various purposes, like creating more sensible navigation in API documentation, defining API lifecycle status with tags like "Beta" that can be filtered out later, or something else entirely. ## Tags Object The document-level `tags` field is an optional field, which is a list of objects describing each [tag](/openapi/tags#tag-object). Tags have at bare minimum a `name`, but can have a `description`, point to `externalDocs`, and potentially have some `x-*` extension properties. ```yaml tags: - name: drinks description: The drinks endpoints. - name: authentication description: The authentication endpoints. ``` Tags can then referenced by [operations](/openapi/paths/operations), with an array allowing multiple tags to be referenced. ```yaml paths: /drinks: get: operationId: listDrinks tags: [drinks] ``` Here is the full specification for the tag object for OpenAPI v3.0-v3.1. <Table data={[ { field: "`name`", type: "String", required: "✅", description: "The name of the tag. **_Must_** be unique in the document." }, { field: "`description`", type: "String", required: "", description: "A description of the tag. This may contain [CommonMark syntax](https://spec.commonmark.org/) to provide a rich description." }, { field: "`externalDocs`", type: "[External Documentation Object](/openapi/external-documentation)", required: "", description: "Additional external documentation for this tag." }, { field: "`x-*`", type: "[Extensions](/openapi/extensions)", required: "", description: "Any number of extension fields can be added to the tag object that can be used by tooling and vendors." } ]} columns={[ { key: "field", header: "Field" }, { key: "type", header: "Type" }, { key: "required", header: "Required" }, { key: "description", header: "Description" } ]} /> ## Human-friendly or machine-friendly Whether a tag should be human readable (e.g. `Publishing Tokens`) or machine-friendly (e.g.: `publishingTokens`) has been a long source of discussion in the OpenAPI world, but consensus is forming around machine-friendly tags more like variable names, in whatever format is preferred: PascalCase, camelCase, snake_case. Then the human friendly name can be passed in one of two locations. 1. For OpenAPI v3.0 and v3.1 users, the commonly used vendor extension `x-displayName` will work in most API documentation tools. 2. The upcoming OpenAPI v3.2 introduces a new `tags.summary` to pass the human-friendly same name. Check to see which of these keywords are supported by documentation tools in the toolchain, and if neither perhaps go with a human-readable `name` until that tool supports `x-displayName` or the OpenAPI is upgraded to v3.2. ## Grouping Tags Tags are used for grouping operations, but sometimes its necessary to group tags, especially in larger APIs with hundreds (or even thousands) of operations. ```yaml tags: - name: stores x-displayName: Stores - name: inventory x-displayName: Inventory - name: employees x-displayName: Employees x-tagGroups: - name: Store Management tags: - stores - inventory - name: Human Resources tags: - employees ``` Operations are then assigned to tags as normal. Tools which understand `x-tagGroups` like [Scalar](https://scalar.com) and [Redoc](https://redocly.com/) will use them to create the nested navigation structures, and tools which do not understand the keyword will build the flat list of operations. ```yaml paths: "/stores": get: operationId: get-stores summary: Get Stores tags: - stores "/inventory": get: operationId: get-inventory summary: Get Inventory tags: - inventory "/employees": get: operationId: get-employees summary: Get Employees tags: - employees ``` In Scalar the above example would look like this. ![Screenshot of Scalar interpreting tag groups](/assets/openapi/guide/scalar-x-tagGroups.png) In OpenAPI v3.2 there will be two new properties which can be used to handle tag grouping and nesting: - `tags.parent` - The `name` of a tag that this tag is nested under. The named tag MUST exist in the API description, and circular references between parent and child tags MUST NOT be used. - `tags.kind` - A machine-readable string to categorize what sort of tag it is. Any string value can be used; common uses are `nav` for Navigation, `badge` for visible badges, `audience` for APIs used by different groups. A [registry of the most commonly used values](https://spec.openapis.org/registry/tag-kind/) is available. It will look something like this. ```yaml tags: - name: account-updates summary: Account Updates description: Account update operations kind: nav - name: partner summary: Partner description: Operations available to the partners network parent: external kind: audience - name: external summary: External description: Operations available to external consumers kind: audience ``` This is not available yet, but will be coming in OpenAPI v3.2 which many tooling providers are excited to start supporting. ## Best practices ### Always define tags before referencing them It's allowed for operations to reference undefined tags (tags not defined in the document-level `tags`, but it is recommended that all tags used are defined here to provide useful documentation and intent for the tags. ### Alphabetize tag definitions Some documentation tools will automatically alphabetize them, some will not, so to make sure they are alphabetized for all documentation tools put them in that order in the OpenAPI document. # Webhooks in OpenAPI Source: https://speakeasy.com/openapi/webhooks Generally speaking REST/HTTP APIs work in a request and response fashion: the client send a request, and the API immediately sends a response back. This is known as the "pull" method. When APIs need time to work through a solution (like generating a large archive for export) or want to allow a client to subscribe for a series of events in the long term (letting them know about every e-commerce sale as it happens) then the "push" method is far more helpful. One of the most popular ways to handle this for a REST/HTTP API is Webhooks. Webhooks let users integrate with APIs in realtime, sending data/events to a URL provided by a user. ```mermaid flowchart LR client([Client]) api[API] api --->|Response| client client -->|Request| api api -.->|Webhook| client ``` Unlike other event-driven technologies, webhooks require the client to be running a web server to handle the incoming HTTP requests. Not all clients will be able to use this, especially frontend/mobile apps, but for any other APIs, microservice, or server-side web apps will be fine. ## How are webhooks used in OpenAPI OpenAPI supports two different approaches to handling webhooks. - `callbacks` - a user will specify a URL in the HTTP request which the API will use to send update(s) about the progress of the request. - `webhooks` - assumes a user has registered a long term (potentially permanent) subscription, and then turns on the firehose of data. Both webhooks and callbacks are a way of defining asynchronous communication with an API. It's possible to use webhooks and callbacks somewhat interchangeably, but it's recommended to stick to the following convention: - If the initial API request triggers a long-running job and the user wants to receive the response asynchronously, use `callbacks`. - If users will be receiving a stream of data over time with a consistent format, you should use `webhooks`. ## Quick example Here is a simple example of a webhook in an inventory management API, that notifies another application when the stock levels of a drink or ingredient change. This could be used to update an e-commerce site or point of sale system, to keep track of stock levels in real time. ```yaml webhooks: stockUpdate: post: summary: Receive stock updates. description: Receive stock updates from the bar, this will be called whenever the stock levels of a drink or ingredient change. requestBody: required: true content: application/json: schema: type: object properties: drink: $ref: "#/components/schemas/Drink" ingredient: $ref: "#/components/schemas/Ingredient" responses: "200": description: The stock update was received successfully. "5XX": $ref: "#/components/responses/APIError" default: $ref: "#/components/responses/UnknownError" ``` Here the `stockUpdate` webhook is defined with a `post` method. The request body contains a JSON object with the drink or ingredient details. One thing to keep in mind here is that the response is not describing what the response is, but what the response should be. The server needs to respond with a `200` response if the stock update was received successfully, or a `5XX` error if there was an issue. ## A short history of webhooks in OpenAPI Callbacks were added to the OpenAPI Specification in [version 3](https://spec.openapis.org/oas/v3.0.0#callback-object) in 2017; webhooks in [version 3.1](https://spec.openapis.org/oas/v3.1.0#oasWebhooks) in 2021. | OpenAPI Version | Date | Notes | | --------------------------------------------- | ------- | --------------- | | [3.0.0](https://spec.openapis.org/oas/v3.0.0) | 2017-07 | Callbacks added | | [3.1.0](https://spec.openapis.org/oas/v3.1.0) | 2021-02 | Webhooks added | In OpenAPI, webhooks and callbacks are defined as follows: - `webhooks` are a top-level entry represented by a map of Path Item Objects or OpenAPI Reference Objects that are keyed by the unique name of the webhook. Webhooks specify what is pushed to a given URL but provide no way of setting the target URL. The subscription itself might be configured by a sales representative, entered during the sign-up process when a user creates an account on your system, or even set by a separate API endpoint (duplicating how callbacks work). - A `callback` is a map of runtime expressions (that represent a URL the callback request is sent to) to a Path Item Object or Reference that defines a request to be initiated by the API provider and a potential response to be returned. The expression, when evaluated at runtime, will resolve to a URL represented in the parameters, request body, or response body of the parent operation. ## Creating a webhook in OpenAPI Let's create an example of webhooks used for notifying another server about upcoming concerts at a club. ### Add a webhook description Add a webhook named `concertAlert` that describes what information will be pushed to a subscriber. The webhook is a `post` method that will be called when a concert is scheduled. The request body contains a string with the details of the concert, but it could just as easily be JSON, XML, or even [JSONL](/openapi/jsonl-responses). ```yaml filename="openapi.yaml" openapi: 3.1.0 info: title: SpeakeasyClub version: 1.0.0 webhooks: concertAlert: post: summary: Concert alert description: Notify the registered URL with details of an upcoming concert requestBody: required: true content: text/plain: schema: type: string example: "The Swing Machine will be playing at 19h30 tomorrow. $10 cover charge." responses: "200": description: Notification received by the external service. ``` Notice that there is no way for a user to register for this alert using the API, nor is the URL on which the user will be notified specified. This is a common pattern for webhooks, where the user registers for the webhook through a different process (like on a control panel or application settings somewhere) and then provides the URL to be notified. ### Add a Subscription Path To allow users to register for alerts without using a callback, you can optionally mimic callback functionality by proving a subscription endpoint in a `/path`. OpenAPI has no way to explicitly link the webhook with the registration. You will have to make this clear to users in your `description` elements or by grouping the operations with tags. ```yaml filename="openapi.yaml" openapi: 3.1.0 info: title: SpeakeasyClub version: 1.0.0 webhooks: concertAlert: post: summary: Concert alert description: Notify the registered URL with details of an upcoming concert requestBody: required: true content: text/plain: schema: type: string example: "The Swing Machine will be playing at 19h30 tomorrow. $10 cover charge." responses: "200": description: Notification received by the external service. paths: /registerForAlert: post: summary: Register for concert alerts description: Register a URL to be called when new concerts are scheduled. requestBody: required: true content: application/json: schema: type: object properties: url: type: string format: uri description: The URL to be notified about approaching concerts. example: url: "https://example.com/notify" responses: "200": description: Registration successful. ``` There is no way to link the webhook with the registration, so you will have to make this clear to users in the `description` elements, or by grouping the operations with tags. ## Creating a Callback in OpenAPI Here is how that same concept may look as a callback registering for more information about a specific concert, which is more in line with what callbacks are about than webhooks. ### Add a Subscription Path Now add a single `/concertUpdates` endpoint that a user can pass a URL to for concert notifications. ```yaml filename="openapi.yaml" openapi: 3.1.0 info: title: SpeakeasyClub version: 1.0.0 paths: /concertUpdates: post: summary: Register for concert updates description: Subscribe to updates such as ticket availability or schedule changes. requestBody: required: true content: application/json: schema: type: object properties: concertId: type: string examples: - abc123 callbackUrl: type: string format: uri description: The URL to receive notifications. example: callbackUrl: "https://example.com/concerts/providerX:abc123" responses: "200": description: Registration successful. ``` To subscribe to this endpoint, a user would send a POST request with a JSON body containing the URL to be notified. For example: ```http POST /concertUpdates HTTP/1.1 Host: example.com Content-Type: application/json { "concertId": "abc123", "callbackUrl": "https://example.com/concerts/providerX:abc123" } ``` ### Add a Callback to the Path Finally, add a callback URL which will receive updates for that concert. The `callbacks` element is a map of runtime expressions to Path Item Objects, where the key is a [runtime expression](https://spec.openapis.org/oas/v3.1.0#runtime-expressions) that will be evaluated at runtime to determine the URL to which the callback request is sent. ```yaml filename="openapi.yaml" openapi: 3.1.0 info: title: SpeakeasyClub version: 1.0.0 paths: /concertUpdates: post: summary: Register for concert updates description: Subscribe to updates such as ticket availability or schedule changes. requestBody: required: true content: application/json: schema: type: object properties: concertId: type: string examples: - abc123 callbackUrl: type: string format: uri description: The URL to receive notifications. example: callbackUrl: "https://example.com/concerts/providerX:abc123" responses: "200": description: Registration successful. callbacks: concertAlert: "{$request.body#/callbackUrl}": post: summary: Concert alert description: Notify the registered URL with details of an upcoming concert. requestBody: required: true content: text/plain: schema: type: string example: "The Swing Machine has been cancelled. Refunds will be sent automatically." responses: "200": description: Notification received by the external service. ``` The callback here is going to be sent to the user provided callbackUrl, which in this instance is defined in the request body. It can be referenced using a [runtime expression](https://spec.openapis.org/oas/v3.1.0#runtime-expressions), for example: `{$request.body#/callbackUrl}`. If the URL was a query string parameter called `callbackUrl` it would instead be `{$request.query.queryUrl}`. Learn more about [runtime expression](https://spec.openapis.org/oas/v3.1.0#runtime-expressions). Selecting parameters in a POST request in this way is called . Read more about the syntax in the specification. ## Syntax Rules The `webhooks` element has identical syntax to the `paths` element. Both are lists of [Path Item Objects](https://spec.openapis.org/oas/v3.1.0#pathItemObject), because a webhook is like a reverse path: just as paths describe endpoints on the server's API, webhooks describe endpoints on the client's API. A `callback` is also a Path Item Object. This means a webhook or callback has all the following path properties available to it: `$ref`, `summary`, `description`, `get`, `put`, `post`, `delete`, `options`, `head`, `patch`, `trace`, `servers`, and `parameters`. ## Callbacks and Webhooks in Speakeasy Speakeasy will [automatically include webhook types](/docs/customize-sdks/webhooks) in generated code and documentation. There's no extra work or configuration require to make Speakeasy enable these features. ## Best practices for OpenAPI Webhooks Here are a few more considerations when designing your API's use of webhooks and callbacks: - If using callbacks, you can manage multiple subscriptions in the same endpoint using multiple parameters — one for each URL. This might be neater than creating one registration path per callback. - If using webhooks, be sure to explain in the summary or description exactly how users can subscribe and unsubscribe, as well as any associated fees for the service. - Any `description` elements may use [CommonMark syntax](https://spec.openapis.org/oas/v3.1.0#rich-text-formatting). - Callbacks and webhooks must have unique names in the schema. The names are case-sensitive. - Make your webhooks idempotent. Sending or receiving the same event multiple times should not cause side effects. - Protect your API from DDoS attacks by making sure webhooks are protected from spam registrations with authentication and that limits are in place for the rate and size of your messages. ## What about AsyncAPI OpenAPI is a general-purpose API specification that can be used for asynchronous APIs, but it is not necessarily optimized for them. If you find that OpenAPI is insufficient for your use case, you should check out [AsyncAPI](https://www.asyncapi.com/). Just be aware that AsyncAPI is still in the early stages of development and is not yet widely supported by the tooling ecosystem. ## Example of webhooks There’s a full example of webhooks in the [Train Travel API](https://github.com/bump-sh-examples/train-travel-api/). # How Speakeasy Supports Our Enterprise Customers Source: https://speakeasy.com/support/enterprise import { Callout, Table } from "@/mdx/components"; ## 1. Automatic SDK Maintenance A core aspect of Speakeasy's value proposition is keeping SDKs up to date with key changes in languages, frameworks, and dependencies. Speakeasy ensures that these updates are propagated to the SDKs maintained for customers, and posts such updates automatically in the SDK pull requests created. <Callout title="Note" type="info"> It is recommended to upgrade to the newest version of Speakeasy to ensure SDKs benefit from the latest updates. Customers using Speakeasy via GitHub Actions will automatically use the most recent version. </Callout> ## 2. Support SLAs Speakeasy is committed to providing rapid response and resolution to any issues you raise. With this in mind, we offer: - A dedicated customer support channel (Slack, Teams). - Prioritized ticket resolution based on severity (P0, P1, P2 SLAs). - SLAs for question response and ticket triage are detailed below. <Table className="px-4 my-8!" containerSize="md" data={[ { priorityLevel: "P0", definition: "Generated Terraform providers or SDKs do not work (severe functional issues)", sla: "First response within 1 hour", initiation: "support@speakeasy.com", credits: "5% of the fee applicable in the month per incident", }, { priorityLevel: "P1", definition: "Generated Terraform providers or SDKs exhibit severe ergonomic or functional issues that significantly impact customers, with no available workaround", sla: "First response within 3 hours", initiation: "support@speakeasy.com", credits: "3% of the fee applicable in the month per incident", }, { priorityLevel: "P2", definition: "Generated Terraform providers or SDKs have bugs with minor ergonomic impact on customers, feature requests", sla: "Prioritize as part of standard Speakeasy development practice, visible in public roadmap", initiation: "support@speakeasy.com", credits: "1% of the fee applicable in the month per incident", }, ]} columns={[ { key: "priorityLevel", header: "Priority level" }, { key: "definition", header: "Definition" }, { key: "sla", header: "SLA" }, { key: "initiation", header: "Initiation" }, { key: "credits", header: "Credits" }, ]} /> ## 3. Uptime SLAs Core systems (code generation, app, CLI) uptime: 99.99% uptime per calendar month - Calculation: Total uptime minutes per calendar month / total minutes per calendar month - Credit on SLA breach: - Uptime between 95% - 99.99%: 10% of the fee applicable in that month - Uptime less than 95%: 20% of the fee applicable in that month Current and historical metrics can be viewed at https://status.speakeasyapi.dev/. In no instance will the total credits in a given month for Technical Support SLA breaches or Uptime SLA breaches exceed the total fee applicable from the customer in that month.