DZone Research Report: A look at our developer audience, their tech stacks, and topics and tools they're exploring.
Getting Started With Large Language Models: A guide for both novices and seasoned practitioners to unlock the power of language models.
Integration refers to the process of combining software parts (or subsystems) into one system. An integration framework is a lightweight utility that provides libraries and standardized methods to coordinate messaging among different technologies. As software connects the world in increasingly more complex ways, integration makes it all possible facilitating app-to-app communication. Learn more about this necessity for modern software development by keeping a pulse on the industry topics such as integrated development environments, API best practices, service-oriented architecture, enterprise service buses, communication architectures, integration testing, and more.
DZone's API Research: Join Us for Our Survey (and Raffle)!
Instant Integrations With API and Logic Automation
In this post, you will learn how you can integrate Large Language Model (LLM) capabilities into your Java application. More specifically, how you can integrate with LocalAI from your Java application. Enjoy! Introduction In a previous post, it was shown how you could run a Large Language Model (LLM) similar to OpenAI by means of LocalAI. The Rest API of OpenAI was used in order to interact with LocalAI. Integrating these capabilities within your Java application can be cumbersome. However, since the introduction of LangChain4j, this has become much easier to do. LangChain4j offers you a simplification in order to integrate with LLMs. It is based on the Python library LangChain. It is therefore also advised to read the documentation and concepts of LangChain since the documentation of LangChain4j is rather short. Many examples are provided though in the LangChain4j examples repository. Especially, the examples in the other-examples directory have been used as inspiration for this blog. The real trigger for writing this blog was the talk I attended about LangChain4j at Devoxx Belgium. This was the most interesting talk I attended at Devoxx: do watch it if you can make time for it. It takes only 50 minutes. The sources used in this blog can be found on GitHub. Prerequisites The prerequisites for this blog are: Basic knowledge about what a Large Language Model is Basic Java knowledge (Java 21 is used) You need LocalAI if you want to run the examples (see the previous blog linked in the introduction on how you can make use of LocalAI). Version 2.2.0 is used for this blog. LangChain4j Examples In this section, some of the capabilities of LangChain4j are shown by means of examples. Some of the examples used in the previous post are now implemented using LangChain4j instead of using curl. How Are You? As a first simple example, you ask the model how it is feeling. In order to make use of LangChain4j in combination with LocalAI, you add the langchain4j-local-ai dependency to the pom file. XML <dependency> <groupId>dev.langchain4j</groupId> <artifactId>langchain4j-local-ai</artifactId> <version>0.24.0</version> </dependency> In order to integrate with LocalAI, you create a ChatLanguageModel specifying the following items: The URL where the LocalAI instance is accessible The name of the model you want to use in LocalAI The temperature: A high temperature allows the model to respond in a more creative way. Next, you ask the model to generate an answer to your question and you print the answer. Java ChatLanguageModel model = LocalAiChatModel.builder() .baseUrl("http://localhost:8080") .modelName("lunademo") .temperature(0.9) .build(); String answer = model.generate("How are you?"); System.out.println(answer); Start LocalAI and run the example above. The response is as expected. Shell I'm doing well, thank you. How about yourself? Before continuing, note something about the difference between LanguageModel and ChatLanguageModel. Both classes are available in LangChain4j, so which one to choose? A chat model is a variation of a language model. If you need a "text in, text out" functionality, you can choose LanguageModel. If you also want to be able to use "chat messages" as input and output, you should use ChatLanguageModel. In the example above, you could just have used LanguageModel and it would behave similarly. Facts About Famous Soccer Player Let’s verify whether it also returns facts about the famous Dutch soccer player Johan Cruijff. You use the same code as before, only now you set the temperature to zero because no creative answer is required. Java ChatLanguageModel model = LocalAiChatModel.builder() .baseUrl("http://localhost:8080") .modelName("lunademo") .temperature(0.0) .build(); String answer = model.generate("who is Johan Cruijff?"); System.out.println(answer); Run the example, the response is as expected. Shell Johan Cruyff was a Dutch professional football player and coach. He played as a forward for Ajax, Barcelona, and the Netherlands national team. He is widely regarded as one of the greatest players of all time and was known for his creativity, skill, and ability to score goals from any position on the field. Stream the Response Sometimes, the answer will take some time. In the OpenAPI specification, you can set the stream parameter to true in order to retrieve the response character by character. This way, you can display the response already to the user before awaiting the complete response. This functionality is also available with LangChain4j but requires the use of a StreamingResponseHandler. The onNext method receives every character one by one. The complete response is gathered in the answerBuilder and futureAnswer. Running this example prints every single character one by one, and at the end, the complete response is printed. Java StreamingChatLanguageModel model = LocalAiStreamingChatModel.builder() .baseUrl("http://localhost:8080") .modelName("lunademo") .temperature(0.0) .build(); StringBuilder answerBuilder = new StringBuilder(); CompletableFuture<String> futureAnswer = new CompletableFuture<>(); model.generate("who is Johan Cruijff?", new StreamingResponseHandler<AiMessage>() { @Override public void onNext(String token) { answerBuilder.append(token); System.out.println(token); } @Override public void onComplete(Response<AiMessage> response) { futureAnswer.complete(answerBuilder.toString()); } @Override public void onError(Throwable error) { futureAnswer.completeExceptionally(error); } }); String answer = futureAnswer.get(90, SECONDS); System.out.println(answer); Run the example. The response is as expected. Shell J o h a n ... s t y l e . Johan Cruijff was a Dutch professional football player and coach who played as a forward. ... Other Languages You can instruct the model by means of a system message how it should behave. For example, you can instruct it to answer always in a different language; Dutch, in this case. This example shows clearly the difference between LanguageModel and ChatLanguageModel. You have to use ChatLanguageModel in this case because you need to interact by means of chat messages with the model. Create a SystemMessage to instruct the model. Create a UserMessage for your question. Add them to a list and send the list of messages to the model. Also, note that the response is an AiMessage. The messages are explained as follows: UserMessage: A ChatMessage coming from a human/user AiMessage: A ChatMessage coming from an AI/assistant SystemMessage: A ChatMessage coming from the system Java ChatLanguageModel model = LocalAiChatModel.builder() .baseUrl("http://localhost:8080") .modelName("lunademo") .temperature(0.0) .build(); SystemMessage responseInDutch = new SystemMessage("You are a helpful assistant. Antwoord altijd in het Nederlands."); UserMessage question = new UserMessage("who is Johan Cruijff?"); var chatMessages = new ArrayList<ChatMessage>(); chatMessages.add(responseInDutch); chatMessages.add(question); Response<AiMessage> response = model.generate(chatMessages); System.out.println(response.content()); Run the example, the response is as expected. Shell AiMessage { text = "Johan Cruijff was een Nederlands voetballer en trainer. Hij speelde als aanvaller en is vooral bekend van zijn tijd bij Ajax en het Nederlands elftal. Hij overleed in 1996 op 68-jarige leeftijd." toolExecutionRequest = null } Chat With Documents A fantastic use case is to use an LLM in order to chat with your own documents. You can provide the LLM with your documents and ask questions about it. For example, when you ask the LLM for which football clubs Johan Cruijff played ("For which football teams did Johan Cruijff play and also give the periods, answer briefly"), you receive the following answer. Shell Johan Cruijff played for Ajax Amsterdam (1954-1973), Barcelona (1973-1978) and the Netherlands national team (1966-1977). This answer is quite ok, but it is not complete, as not all football clubs are mentioned and the period for Ajax includes also his youth period. The correct answer should be: Years Team 1964-1973 Ajax 1973-1978 Barcelona 1979 Los Angeles Aztecs 1980 Washington Diplomats 1981 Levante 1981 Washington Diplomats 1981-1983 Ajax 1983-1984 Feyenoord Apparently, the LLM does not have all relevant information and that is not a surprise. The LLM has some basic knowledge, it runs locally and has its limitations. But what if you could provide the LLM with extra information in order that it can give an adequate answer? Let’s see how this works. First, you need to add some extra dependencies to the pom file: XML <dependency> <groupId>dev.langchain4j</groupId> <artifactId>langchain4j</artifactId> <version>${langchain4j.version}</version> </dependency> <dependency> <groupId>dev.langchain4j</groupId> <artifactId>langchain4j-embeddings</artifactId> <version>${langchain4j.version}</version> </dependency> <dependency> <groupId>dev.langchain4j</groupId> <artifactId>langchain4j-embeddings-all-minilm-l6-v2</artifactId> <version>${langchain4j.version}</version> </dependency> Save the Wikipedia text of Johan Cruijff to a PDF file and store it in src/main/resources/example-files/Johan_Cruyff.pdf. The source code to add this document to the LLM consists of the following parts: The text needs to be embedded; i.e., the text needs to be converted to numbers. An embedding model is needed for that, for simplicity you use the AllMiniLmL6V2EmbeddingModel. The embeddings need to be stored in an embedding store. Often a vector database is used for this purpose, but in this case, you can use an in-memory embedding store. The document needs to be split into chunks. For simplicity, you split the document into chunks of 500 characters. All of this comes together in the EmbeddingStoreIngestor. Add the PDF to the ingestor. Create the ChatLanguageModel just like you did before. With a ConversationalRetrievalChain, you connect the language model with the embedding store and model. And finally, you execute your question. Java EmbeddingModel embeddingModel = new AllMiniLmL6V2EmbeddingModel(); EmbeddingStore<TextSegment> embeddingStore = new InMemoryEmbeddingStore<>(); EmbeddingStoreIngestor ingestor = EmbeddingStoreIngestor.builder() .documentSplitter(DocumentSplitters.recursive(500, 0)) .embeddingModel(embeddingModel) .embeddingStore(embeddingStore) .build(); Document johanCruiffInfo = loadDocument(toPath("example-files/Johan_Cruyff.pdf")); ingestor.ingest(johanCruiffInfo); ChatLanguageModel model = LocalAiChatModel.builder() .baseUrl("http://localhost:8080") .modelName("lunademo") .temperature(0.0) .build(); ConversationalRetrievalChain chain = ConversationalRetrievalChain.builder() .chatLanguageModel(model) .retriever(EmbeddingStoreRetriever.from(embeddingStore, embeddingModel)) .build(); String answer = chain.execute("Give all football teams Johan Cruijff played for in his senior career"); System.out.println(answer); When you execute this code, an exception is thrown. Shell Exception in thread "main" java.lang.RuntimeException: java.lang.RuntimeException: java.io.InterruptedIOException: timeout at dev.langchain4j.internal.RetryUtils.withRetry(RetryUtils.java:29) at dev.langchain4j.model.localai.LocalAiChatModel.generate(LocalAiChatModel.java:98) at dev.langchain4j.model.localai.LocalAiChatModel.generate(LocalAiChatModel.java:65) at dev.langchain4j.chain.ConversationalRetrievalChain.execute(ConversationalRetrievalChain.java:65) at com.mydeveloperplanet.mylangchain4jplanet.ChatWithDocuments.main(ChatWithDocuments.java:55) Caused by: java.lang.RuntimeException: java.io.InterruptedIOException: timeout at dev.ai4j.openai4j.SyncRequestExecutor.execute(SyncRequestExecutor.java:31) at dev.ai4j.openai4j.RequestExecutor.execute(RequestExecutor.java:59) at dev.langchain4j.model.localai.LocalAiChatModel.lambda$generate$0(LocalAiChatModel.java:98) at dev.langchain4j.internal.RetryUtils.withRetry(RetryUtils.java:26) ... 4 more Caused by: java.io.InterruptedIOException: timeout at okhttp3.internal.connection.RealCall.timeoutExit(RealCall.kt:398) at okhttp3.internal.connection.RealCall.callDone(RealCall.kt:360) at okhttp3.internal.connection.RealCall.noMoreExchanges$okhttp(RealCall.kt:325) at okhttp3.internal.connection.RealCall.getResponseWithInterceptorChain$okhttp(RealCall.kt:209) at okhttp3.internal.connection.RealCall.execute(RealCall.kt:154) at retrofit2.OkHttpCall.execute(OkHttpCall.java:204) at dev.ai4j.openai4j.SyncRequestExecutor.execute(SyncRequestExecutor.java:23) ... 7 more Caused by: java.net.SocketTimeoutException: timeout at okio.SocketAsyncTimeout.newTimeoutException(JvmOkio.kt:147) at okio.AsyncTimeout.access$newTimeoutException(AsyncTimeout.kt:158) at okio.AsyncTimeout$source$1.read(AsyncTimeout.kt:337) at okio.RealBufferedSource.indexOf(RealBufferedSource.kt:427) at okio.RealBufferedSource.readUtf8LineStrict(RealBufferedSource.kt:320) at okhttp3.internal.http1.HeadersReader.readLine(HeadersReader.kt:29) at okhttp3.internal.http1.Http1ExchangeCodec.readResponseHeaders(Http1ExchangeCodec.kt:178) at okhttp3.internal.connection.Exchange.readResponseHeaders(Exchange.kt:106) at okhttp3.internal.http.CallServerInterceptor.intercept(CallServerInterceptor.kt:79) at okhttp3.internal.http.RealInterceptorChain.proceed(RealInterceptorChain.kt:109) at okhttp3.internal.connection.ConnectInterceptor.intercept(ConnectInterceptor.kt:34) at okhttp3.internal.http.RealInterceptorChain.proceed(RealInterceptorChain.kt:109) at okhttp3.internal.cache.CacheInterceptor.intercept(CacheInterceptor.kt:95) at okhttp3.internal.http.RealInterceptorChain.proceed(RealInterceptorChain.kt:109) at okhttp3.internal.http.BridgeInterceptor.intercept(BridgeInterceptor.kt:83) at okhttp3.internal.http.RealInterceptorChain.proceed(RealInterceptorChain.kt:109) at okhttp3.internal.http.RetryAndFollowUpInterceptor.intercept(RetryAndFollowUpInterceptor.kt:76) at okhttp3.internal.http.RealInterceptorChain.proceed(RealInterceptorChain.kt:109) at dev.ai4j.openai4j.ResponseLoggingInterceptor.intercept(ResponseLoggingInterceptor.java:21) at okhttp3.internal.http.RealInterceptorChain.proceed(RealInterceptorChain.kt:109) at dev.ai4j.openai4j.RequestLoggingInterceptor.intercept(RequestLoggingInterceptor.java:31) at okhttp3.internal.http.RealInterceptorChain.proceed(RealInterceptorChain.kt:109) at dev.ai4j.openai4j.AuthorizationHeaderInjector.intercept(AuthorizationHeaderInjector.java:25) at okhttp3.internal.http.RealInterceptorChain.proceed(RealInterceptorChain.kt:109) at okhttp3.internal.connection.RealCall.getResponseWithInterceptorChain$okhttp(RealCall.kt:201) ... 10 more Caused by: java.net.SocketException: Socket closed at java.base/sun.nio.ch.NioSocketImpl.endRead(NioSocketImpl.java:243) at java.base/sun.nio.ch.NioSocketImpl.implRead(NioSocketImpl.java:323) at java.base/sun.nio.ch.NioSocketImpl.read(NioSocketImpl.java:346) at java.base/sun.nio.ch.NioSocketImpl$1.read(NioSocketImpl.java:796) at java.base/java.net.Socket$SocketInputStream.read(Socket.java:1099) at okio.InputStreamSource.read(JvmOkio.kt:94) at okio.AsyncTimeout$source$1.read(AsyncTimeout.kt:125) ... 32 more This can be solved by setting the timeout of the language model to a higher value. Java ChatLanguageModel model = LocalAiChatModel.builder() .baseUrl("http://localhost:8080") .modelName("lunademo") .temperature(0.0) .timeout(Duration.ofMinutes(5)) .build(); Run the code again, and the following answer is received, which is correct. Shell Johan Cruijff played for the following football teams in his senior career: - Ajax (1964-1973) - Barcelona (1973-1978) - Los Angeles Aztecs (1979) - Washington Diplomats (1980-1981) - Levante (1981) - Ajax (1981-1983) - Feyenoord (1983-1984) - Netherlands national team (1966-1977) Using a 1.x version of LocalAI gave this response, which was worse. Shell Johan Cruyff played for the following football teams: - Ajax (1964-1973) - Barcelona (1973-1978) - Los Angeles Aztecs (1979) The following steps were used to solve this problem. When you take a closer look at the PDF file, you notice that the information about the football teams is listed in a table next to the regular text. Remember that splitting the document was done by creating chunks of 500 characters. So, maybe this splitting is not executed well enough for the LLM. Copy the football teams in a separate text document. Plain Text Years Team Apps (Gls) 1964–1973 Ajax 245 (193) 1973–1978 Barcelona 143 (48) 1979 Los Angeles Aztecs 22 (14) 1980 Washington Diplomats 24 (10) 1981 Levante 10 (2) 1981 Washington Diplomats 5 (2) 1981–1983 Ajax 36 (14) 1983–1984 Feyenoord 33 (11) Add both documents to the ingestor. Java Document johanCruiffInfo = loadDocument(toPath("example-files/Johan_Cruyff.pdf")); Document clubs = loadDocument(toPath("example-files/Johan_Cruyff_clubs.txt")); ingestor.ingest(johanCruiffInfo, clubs); Run this code and this time, the answer was correct and complete. Shell Johan Cruijff played for the following football teams in his senior career: - Ajax (1964-1973) - Barcelona (1973-1978) - Los Angeles Aztecs (1979) - Washington Diplomats (1980-1981) - Levante (1981) - Ajax (1981-1983) - Feyenoord (1983-1984) - Netherlands national team (1966-1977) It is therefore important that the sources you provide to an LLM are split wisely. Besides that, the used technologies improve in a rapid way. Even while writing this blog, some problems were solved in a couple of weeks. Updating to a more recent version of LocalAI for example, solved one way or the other the problem with parsing the single PDF. Conclusion In this post, you learned how to integrate an LLM from within your Java application using LangChain4j. You also learned how to chat with documents, which is a fantastic use case! It is also important to regularly update to newer versions as the development of these AI technologies improves continuously.
A mix of anticipation and dread washes over me as I open a new inbound email with an attached specification file. With a heavy sigh, I begin scrolling through its contents, only to be greeted by disappointment yet again. The API request bodies in this specification file suffer from a lack of essential details, specifically the absence of the actual properties of the HTTP call. This makes it difficult to determine the expectations and behavior of the API. Not only will API consumers have a hard time understanding the API, but the lack of properties also hinders the use of external libraries for validation, analysis, or auto-generation of output (e.g., API mocking, testing, or liblab's auto SDK generation). After encountering hundreds of specification files (referred to as specs) in my role at liblab, I’ve come to the conclusion that most spec files are in varying degrees of incompletion. Some completely disregard the community standard and omit crucial information while others could use some tweaking and refinement. This has inspired me to write this article with the goal of enhancing the quality of your spec files. It just so happens that this goal also aligns with making my job easier. In the upcoming sections, we'll go over three common issues that make your OpenAPI spec fall short and examine possible solutions for them. By the end of this article, you’ll be able to elevate your OpenAPI spec, making it more user-friendly for API consumers, including developers, QA engineers, and other stakeholders. Three Reasons Why Your OpenAPI Spec Sucks You’re Still Using Swagger Look, I get it. A lot of us still get confused about the differences between Swagger and OpenAPI. To make things simple you can think of Swagger as the former name of OpenAPI. Many tools are still using the word "Swagger" in their names but this is primarily due to the strong association and recognition that the term Swagger had gained within the developer community. If your “Swagger” spec is actually an OpenAPI spec (indicated by the presence of "openapi: 3.x.x" at the beginning), all you need to do is update your terminology. If you’re actually using a Swagger spec (a file that begins with "swagger: 2.0”), it's time to consider an upgrade. Swagger has certain limitations compared to OpenAPI 3, and as newer versions of OpenAPI are released, transitioning will become increasingly challenging. Notable differences: OpenAPI 3 has support for oneOf and anyOf that Swagger does not provide. Let us look at this example: openapi: 3.0.0 info: title: Payment API version: 1.0.0 paths: /payments: post: summary: Create a payment requestBody: required: true content: application/json: schema: oneOf: - $ref: "#/components/schemas/CreditCardPayment" - $ref: "#/components/schemas/OnlinePayment" - $ref: "#/components/schemas/CryptoPayment" responses: "201": description: Created "400": description: Bad Request In OpenAPI 3, you can explicitly define that the requestBody for a /payments POST call can be one of three options: CreditCardPayment, OnlinePayment, or CryptoPayment. However, in Swagger you would need to create a workaround by adding an object with optional fields for each payment type: swagger: "2.0" info: title: Payment API version: 1.0.0 paths: /payments: post: summary: Create a payment consumes: - application/json produces: - application/json parameters: - name: body in: body required: true schema: $ref: "#/definitions/Payment" responses: "201": description: Created "400": description: Bad Request definitions: Payment: type: object properties: creditCardPayment: $ref: "#/definitions/CreditCardPayment" onlinePayment: $ref: "#/definitions/OnlinePayment" cryptoPayment: $ref: "#/definitions/CryptoPayment" # Make the properties optional required: [] CreditCardPayment: type: object # Properties specific to CreditCardPayment OnlinePayment: type: object # Properties specific to OnlinePayment CryptoPayment: type: object # Properties specific to CryptoPayment This example does not resemble the OpenAPI 3 implementation fully as the API consumer has to specify the type they are sending through a property field, and they also might send more than of the fields since they are all marked optional. This approach lacks the explicit validation and semantics provided by the oneOf keyword in OpenAPI 3. In OpenAPI, you can describe multiple server URLs, while in Swagger you’re bound to only one: { "swagger": "2.0", "info": { "title": "Sample API", "version": "1.0.0" }, "host": "api.example.com", "basePath": "/v1", ... } openapi: 3.0.0 info: title: Sample API version: 1.0.0 servers: - url: http://api.example.com/v1 description: Production Server - url: https://sandbox.api.example.com/v1 description: Sandbox Server ... You’re Not Using Components One way of making an OpenAPI spec more readable is by removing any unnecessary duplication — the same way as a programmer would with their code. If you find that your OpenAPI spec is too messy and hard to read you might be under-utilizing the components section. Components provide a powerful mechanism for defining reusable schemas, parameters, responses, and other elements within your specification. Let's take a look at the following example that does not utilize components: openapi: 3.0.0 info: title: Nested Query Example version: 1.0.0 paths: /users: get: summary: Get users with nested query parameters parameters: - name: filter in: query schema: type: object properties: name: type: string age: type: number address: type: object properties: city: type: string state: type: string country: type: string zipcode: type: string ... /user/{id}/friend: get: summary: Get a user's friend parameters: - name: id in: path schema: type: string - name: filter in: query schema: type: object properties: name: type: string age: type: number address: type: object properties: city: type: string state: type: string country: type: string zipcode: type: string ... The filter parameter in this example is heavily nested and can be challenging to follow. It is also used in its full length by two different endpoints. We can consolidate this behavior by leveraging component schemas: openapi: 3.0.0 info: title: Nested Query Example with Schema References version: 1.0.0 paths: /users: get: summary: Get users with nested query parameters parameters: - name: filter in: query schema: $ref: "#/components/schemas/UserFilter" ... /user/{id}/friend: get: summary: Get a user's friend parameters: - name: id in: path schema: type: string - name: filter in: query schema: $ref: "#/components/schemas/UserFilter" ... components: schemas: UserFilter: type: object properties: name: type: string age: type: number address: $ref: "#/components/schemas/AddressFilter" AddressFilter: type: object properties: city: type: string state: type: string country: type: string zipcode: type: string The second example is clean and readable. By creating UserFilter and AddressFilter, we can reuse those schemas throughout the spec file, and if they ever change, we will only have to update them in one place. You’re Not Using Descriptions, Examples, Formats, or Patterns You finally finished porting all your endpoints and models into your OpenAPI spec. It took you a while, but now you can finally share it with development teams, QA teams, and even customers. Shortly after you share your spec with the world, the questions start arriving: “What does this endpoint do? What’s the purpose of this parameter? When should the parameter be used?” Lets take a look at this example: openapi: 3.0.0 info: title: Sample API version: 1.0.0 paths: /data: post: summary: Upload user data requestBody: required: true content: application/json: schema: type: object properties: name: type: string age: type: integer email: type: string responses: "200": description: Successful response We can deduce from it that data needs to be uploaded, but questions remain: What specific data should be uploaded? Is it the data pertaining to the current user? Whose name, age, and email do these attributes correspond to? openapi: 3.0.0 info: title: Sample API version: 1.0.0 paths: /data: post: summary: Upload user data description: > Endpoint for uploading new user data to the system. This data will be used for personalized recommendations and analysis. Ensure the data is in a valid JSON format. requestBody: required: true content: application/json: schema: type: object properties: name: type: string description: The name of a new user. age: type: integer description: The age of a new user. email: type: string description: The email address of a new user. responses: "200": description: Successful response You can’t always control how your API was structured, but you can control the descriptions you give it. Reduce the number of questions you receive by adding useful descriptions wherever possible. Even after incorporating descriptions, you still might be asked about various aspects of your OpenAPI spec. At this point, you might be thinking, "Sharon, you deceived me! I added all those descriptions yet the questions keep on coming.” Before you give up, have you thought about adding examples? Lets take a look at this parameter: parameters: - name: id in: path required: true schema: type: string description: The user id. Based on the example, we understand that "id" is a string and serves as the user's identifier. However, despite your QA team relying on your OpenAPI spec for their tests, they are encountering issues. They inform you that they are passing a string, yet the API call fails. “That’s because you’re not passing valid ids,” you tell them. You rush to add an example to your OpenAPI spec: parameters: - name: id in: path required: true schema: type: string example: e4bb1afb-4a4f-4dd6-8be0-e615d233185b description: The user id. After your update your spec a follow up question arrives: would "d0656a1f-1lac-4n7b-89de-3e8ic292b2e1” be a good example as well? The answer is no since the characters 'l' and 'n' in the example are not valid hexadecimal characters, making them illegal in the UUID format: parameters: - name: id in: path required: true schema: type: string format: uuid example: e4bb1afb-4a4f-4dd6-8be0-e615d233185b description: The user id. Finally, your QA team has all the information they need to interact with the endpoints that use this parameter. But what if a parameter is not of a common format? That’s when regex patterns come in: parameters: - name: id in: path required: true schema: type: string pattern: "[a-f0-9]{32}" example: 2675b703b9d4451f8d4861a3eee54449 description: A 32-character unique user ID. By using the pattern field, you can define custom validation rules for string properties, enabling more precise constraints on the data accepted by your API. You can read more about formats, examples, and patterns here. Conclusion This list of shortcomings is certainly not exhaustive, but the most common and easily fixable ones presented in this post include upgrading from Swagger, utilizing components effectively, and providing comprehensive documentation. By making these improvements, you are laying the foundation for successful API documentation. When working on your spec, put yourself in the shoes of a new API consumer, since this is their initial interaction with the API. Ensure that it is well-documented and easy to comprehend, and set the stage for a positive developer experience.
Declarative programming is based upon the "what" instead of the "how." It's probably easier to explain by using a real-world example of something you might want to achieve using programming. Imagine you want to create an API endpoint that allows users to register in your backend while simultaneously making a payment towards Stripe. This could be for something that's a subscription-based service, where you charge people for access to something. In a "how" world, this requires an understanding of the HTTP standard to be able to invoke Stripe's API. It requires knowledge of security concepts such as blowfish hashing passwords and connecting to your database. In addition, knowledge about how Stripe's API is tied together. The amount of knowledge you need to do this simple task requires years and sometimes decades of training. In a "what" programming language, implying a declarative programming language, it only requires you to understand how to decorate an invocation towards Stripe's API using a modal dialog such as the following. Ten years of training reduced to a simple modal dialog you can probably understand in a few minutes if you try. The cost savings should be obvious at this point. With declarative programming, any human being can, in theory, create fairly complex applications without prior knowledge about software development. Hyper IDE Hyper IDE allows you to create workflows. A workflow is a chained sequence of actions, similar to functions, where each action might produce a set of outputs, and the output of one action can become the input to the next action. This allows you, in theory, to create anything you can create using any other programming language, except 10,000 times easier, because there's no "how" in action. The above action, for instance, will internally read settings from your configuration object. Then, it will decorate and create an HTTP POST invocation towards Stripe. Once Stripe returns, it will sanity check the result to make sure the invocation was a success before allowing control to "flow" to the next action. If an error occurs, it will abort the execution of your workflow by throwing an exception. The above action does not require you to understand HTTP, how to read from your configuration settings, how to decorate a Stripe API invocation, or any of the other "how constructs." It simply cares about "the what," which is. Who is paying? What payment method is the person paying with? And what's the price reference? As an optional question, it asks the software developer if he or she wants to have Stripe automatically collect tax or not. Everything else is "hidden" behind its implementation, making it 100% perfectly "encapsulated." In fact, the software developer using the above dialog doesn't even need to know that it's creating an HTTP POST invocation towards Stripe. From the software developer's perspective, the fact that the action is using HTTP is completely hidden and has "vanished into Magic"... 40+ years of software development experience reduced to a simple dialog. There is no difference in the quality of the above modal dialog and the best possible solution a senior software developer could, in theory, create — quite the opposite, in fact, since the above modal dialog typically will be reused in thousands of apps, resulting in much more stable and secure code in the end due to being "debugged by the world." HyperLambda and Meta Programming Declarative constructs such as the above are typically facilitated by Meta programming. Meta programming implies code that creates code. Meta programming needs to be highly dynamic, it's therefor typically created using XML or JSON. However, HyperLambda is simply superior as a mechanism to describe workflows because even though the intention is to use declarative programming constructs, you still can actually read the code it creates, contrary to what you end up with if you're using JSON, YAML, or XML. If you've ever tried to "debug an XML file," you'll understand what I mean here. To illustrate that point, let me show you the end result of clicking the above "OK" button and what the modal dialog creates for you. Shell /* * Creates a new Stripe payment for the given [customer_id], using the * specified [payment_method_id], for the given [amount], in the given [currency]. * * Will use your Stripe API token found from your settings as it's interacting * with the Stripe API. */ execute:magic.workflows.actions.execute name:stripe-payment-create filename:/modules/stripe/workflows/actions/stripe-payment-create.hl arguments customer_id:x:--/execute/=stripe-customer-create/*/customer_id payment_method_id:x:--/execute/=stripe-payment-method-create/*/payment_method_id amount:x:@.arguments/*/amount currency:usd description:Payment from Magic metadata username:x:@.arguments/*/username Now, imagine how the above would look if we used XML or JSON. It would basically be impossible to read. In addition, since it's actually humanly readable, you can edit the code using a code editor. Below is a screenshot of me editing HyperLambda using the autocomplete feature from Hyper IDE, which automatically suggests code for me. HyperLambda, Hyper IDE, and Workflows basically provide a superior development experience, requiring close to zero knowledge about coding, making me 1,000 times more productive. Or, to rephrase... Where the Machine Creates the Code Conclusion Declarative programming through Low-Code, No-Code, HyperLambda, Hyper IDE, and Workflows, allows me to work 1,000 times faster than a traditional programming language. In addition, it results in higher-quality code and more secure solutions since it becomes impossible to overlook anything. In the video below, I go through how to create an HTTP POST endpoint that allows users to register in your backend and make a purchase using Stripe. If you asked the average senior software developer how much time he or she would need to achieve the same, the answer would be at least one day, possibly a week. I created it in 20 minutes while explaining on YouTube what I do and how everything works. When I'm done, I connect my API to a custom GPT using natural language, and I invoke my API with ChatGPT and OpenAI's models, using nothing but natural language as my "user interface." Software development today is in its infancy. To some extent, the way we're creating software today can be compared to the way monkeys created tools 2 million years ago. My bet is that 50 years from now, the way we produce code today with OOP, files, keywords, variables, and function invocation — while focusing on "the how" — will be the laughing joke for future generations. Alan Kay said in 1998 that "The computer revolution hasn't even started." Twenty-five years later, I still agree with him. We've moved a few inches beyond the equivalent of the steam engines from 1865, but we've barely tapped into 0.1% of the potential we have for a true "computer revolution." And to truly embrace the computer revolution, we need to make software development available to the masses. Today, 0.3% of the world population can create software to some extent. Our means to accomplish the above is Magic.
APIs need to change over time. Features are added, bugs are fixed, and changes are made. How can you introduce and track changes without breaking client applications? API versioning is the answer. By versioning your API, you work toward building a robust and scalable product. What Is API Versioning? Versioning an API is the process that allows tracking changes and managing the API's various iterations. Essentially, versioning allows you to create multiple API versions that that coexist but operate independently of each other. That way, new features can be added, updates can be made, and old features can be removed with minimal disruption of service to the user. Why Is API Versioning Important? Proper versioning is a crucial step to keep projects flexible and ensure compatibility with existing and new tools. Without proper versioning, modifications to the API could cause unexpected errors and result in disruptions for the client. You’ll likely need to make changes to your API over time, so it’s a good idea to analyze whether or not implementing proper API versioning from the start would be a good idea. A good API versioning strategy not only helps to make projects more flexible, but it can also make projects compatible with more tools and protect backwards compatibility. Over the course of the project, it can also help lower the cost of introducing new features and help communicate changes clearly to the users. Since each API version number gets its own release notes, migration guides, and updated documentation, this strategy promotes a trusting relationship with the user. When Should You Version an API? If you're going to introduce major changes to the API, it would be a good idea to consider adopting an API versioning strategy. As different versions become available, users can incrementally opt-in to new features at their own pace. Versioning can also facilitate making security updates without forcing API users into upgrades that would require downtime to incorporate. In a situation where the API will support multiple client platforms, API versioning will allow the user to stick with their platform's SDK without being worried about updates for other platforms. When Should You Not Version an API? Versioning isn't the best solution for every situation, though. Developing a full versioning strategy for minor updates or bug fixes will more likely add confusion than benefits. Also, in situations where there are only one or two users, such as an API that will only be used internally, it's probably more practical to just update both server and client at once. Same goes if you’re introducing a non-breaking or temporary change, or something on a branch that won't impact any clients. How to Do API Versioning If you think API versioning will be a good fit, you need to understand how to adapt API versioning to suit your needs. One of the first things you’ll want to consider is how you want to label your versioning. There are a few options: Semantic Versioning (commonly referred to as SemVer) follows a MAJOR.MINOR.PATCH format. For more information on semantic versioning, semver.org is a good resource. It’s helpful for tracking backward-compatible changes, functionality, and bug fixes. Each of the major breaking changes are incremented as a new major version number, while backward-compatible additions and bug fixes are each just a minor version number. Date-based versioning tags make every API version number the date it was released, which might be useful in some situations where a chronological sequence of releases is more relevant than semantic clarity. Endpoint-based versioning may be helpful in limited situations where the scope of the version will only affect certain endpoints with independent resources. There isn’t consensus on the “best” approach; it really depends on what information will help you better track the changes made to the API. Analyzing your needs and desired results will help you decide which system will work best for you. Types of API Versioning Next, you need to decide how the user specifies which API version they want to use. Here are some options: Versioning Type Basics Example Positives Negatives URI Versioning The version numbers are incorporated into a URL path. http://www.example.com/api/1/products Easy to understand and implement. Clearly separated API versions. Can become cluttered. Not recommended by REST architecture. Query Parameter The version number is appended as a query parameter in the API endpoint. http://www.example.com/api/products?version=1 Clear separation of API versions. Easy to implement. Less intuitive for API consumers. Can result in long cluttered URLs. Header Based The version number is a specific and unique header field. curl -H “Accepts-version: 1.0” <http://www.example.com/api/products> Follows REST principles. Keeps URI focused on the resources. Less intuitive. More effort is needed to check API request. Content Negotiation The version is based on the representational state or media type. curl -H “Accept: application/vnd.xm.device+json; version=1” <http://www.example.com/api/products> Smaller footprint. No need for URI routing rules. Versions resource representations instead of entire API. Less accessible for testing and exploring via browser. Again, each of these techniques have different objectives and advantages, so your specific project requirements should determine which technique is the best for you. How to Build an API Versioning Strategy Once you’ve planned out what methods and techniques will best suit your constraints and objectives, you’re ready to formulate your strategy according to API versioning best practices. You’ll need to assess the project scope and define what your versioning policy will be. REST (Representational State Transfer) is a popular API architecture for building web services in which resources are accessed via standard HTTP methods. Using versioning with a REST API allows the developer to add new features to the API, fix bugs, and remove old functionality without breaking anything for the API consumers. If you’re building a REST API, there are a few principles regarding versioning that you might want to keep in mind. Here are some of those recommended API versioning strategies: 1. Communicate Changes Clearly The whole point of using a REST API is so there’s no confusion between client and server about where to access resources. That completely breaks down if you haven’t clearly communicated to the API consumers when things change on the server. You’ll need to consider release notes, migration guides, and updated API documentation to keep everyone on the same page. Perhaps it’d even be worth considering a longer time table to give users enough time to prepare for and implement updates. 2. Use Semantic Versioning We talked about some of the other options, but semantic versioning is best in line with REST principles. Why? Because REST APIs are stateless; endpoints aren’t affected by outside constraints and function independently from one another. They (in theory, unless you really need it) shouldn’t be fixed to anything in the real world affecting their output, like the date of the most recent new version. Setting up SemVer isolates the endpoints from anything resembling state even further. 3. Maintain Backward Compatibility When Possible REST APIs are uniform and consistent. In an ideal world, there would never be any breaking changes. In reality, that’s difficult to implement long-term, but always lean on the side of backward compatibility. For example, new endpoint parameters should have default values. New features should get their own new endpoints and their own new version. Removing existing fields from API responses is also frowned upon for this reason, especially if you have a good deprecation strategy. 4. Deprecate Old Versions Gradually What does a good deprecation strategy look like? A clear timeline. Support old versions during the deprecation period and make sure that deprecation endpoints are recorded clearly in the API documentation. Also, be clear with the user. Make sure they’re on the same page about why older versions are being deprecated, what the benefits of upgrading to the new version are, what issues they might face, and how they make solve those issues. Ample support during the transition period will help minimize disruptions and promote trust between the developer and API consumers. API Versioning Best Practices Many aspects of your API versioning strategy will be dependent on factors unique to your application, but there are some general guidelines of API versioning best practices to take into consideration. 1. Prioritize the Docs The current state of the entire API should be reflected in comprehensive documentation, customized to the latest API version. Make sure that there are clear instructions on how changes should be introduced with each new version so no user gets confused — you’d be surprised how little friction it takes to make some users jump ship. 2. Keep the Line Open With the Clients Good communication is key. Understand what your consumers need and how each new version will affect their workflow, not just yours. Establish good channels of communication in advance to inform users of each upcoming change and each new version. Those channels also let you gather feedback from users to understand what their needs are and what their expectations are, and that’ll help you build a roadmap for the future of your API. 3. Plan for Security and Scalability While most of API versioning focuses on the functional aspect of the API, security and scalability should be taken into consideration. As new versions are introduced that protect against security threats, older versions may continue to have those since-fixed vulnerabilities. Also, if you build a good API, you’ll start eventually getting increased usage (both over time and in quick spikes) and larger data volumes. The solution? Bake in automated security checks, vulnerability assessments, and scalable practices from the start. Make security updates and patch versions a priority for every version you support, not just the latest version. That includes every major or minor patch, even the since-replaced ones. This is an even bigger area where communication is crucial, since there may even be legal obligations to inform API users of how their data security is being protected in each new update. 4. Test Thoroughly You want to catch as many issues as possible before the API gets to the user. Conduct unit tests, integration tests, and regression tests for each new version. We’ve all been through the frustration of doing an upgrade on one part of a project just to find that we accidentally broke everything else. Thorough testing at each stage of API versioning helps avoid those situations and ensures a reliable product for the user. Automated tools can greatly streamline the process. How to Test API Versions To start, you want to thoroughly test the new API version separately to ensure that it meets all the functional specifications that the new API version is supposed to meet. There are a couple ways to do this: 1. Unit Testing Unit testing involves testing individual pieces of code. For example, does each endpoint still function like expected? Take an endpoint that just takes in a letter, and if it’s within a certain range of ASCII values, it’ll shift the letter by however many places you specify. Here’s a function that does that: const shiftLetter = (letter, key, rangeStart, rangeEnd) => { const rangeLength = rangeEnd - rangeStart; const code = letter.charCodeAt(0); if (rangeStart <= code && code < rangeEnd) { let n = code - rangeStart + key; if (n < 0) n = rangeLength - Math.abs(n) % rangeLength; return String.fromCharCode((n % rangeLength) + rangeStart); } else return letter; }; These examples are from an Algolia article about unit testing. If we have standardized our API, we don’t even need to test this over HTTP requests since that part acts predictably. We can just write a little function like this to test this particular unit of code: const testShiftLetter = () => { if (shiftLetter("L", 3, 65, 91) != "O") throw "Assertion error"; // test basic case if (shiftLetter("s", 14, 65, 122) throw "Assertion error"; // test wrap around, and custom ranges }; All it does is throw an error if the function doesn’t produce the correct result. You can also measure the performance of individual units here. Each new API version requires that you rerun these tests to make sure each individual piece of the API still works as you expect, so you should build this into your automated workflow, perhaps using a tool like GitStream. 2. Integration Testing Integration testing is very similar to unit testing (some don’t even make the distinction). The difference is that now we’re testing how units of code work together to produce the right result. Here's a more complex example from that same article: const testCaesar = () => { if (caesar("HELLO", 1) != "IFMMP") throw "Assertion error"; // test basic case if (caesar(caesar("DECRYPTED TEXT", 19), -19) != "DECRYPTED TEXT") throw "Assertion error"; // test negative keys for decryption }; See how it tests expected output even in edge cases? 3. System Testing The last type of testing involves using an application built with the API, testing how everything works together. This is harder to implement, but since you’ve built such great documentation and migration guides for each new version, you likely have demos built with your API that you can test with. Conclusion It may seem daunting to consider all these factors at the beginning of a project, but the time and effort now will pay dividends through the entire lifecycle of the project. If you're in a situation where it makes sense to create an API versioning strategy, the hard work right now will definitely be worth it! Thoughtful planning and implementation of best practices will result in robust scalable APIs and ensure long-term stability and adaptability. It's important to remember that software development is an evolving landscape, so we as devs have to keep up to date with improved best practices and new methods. Doing that puts you well on your way towards creating APIs with smooth transitions between versions, enhancing the end user experience, and helping you to build strong relationships with satisfied customers.
This tutorial illustrates B2B push-style application integration with APIs and internal integration with messages. We have the following use cases: Ad Hoc Requests for information (Sales, Accounting) that cannot be anticipated in advance. Two Transaction Sources: A) internal Order Entry UI, and B) B2B partner OrderB2B API. The Northwind API Logic Server provides APIs and logic for both transaction sources: Self-Serve APIs to support ad hoc integration and UI dev, providing security (e.g., customers see only their accounts). Order Logic: enforcing database integrity and Application Integration (alert shipping). A custom API to match an agreed-upon format for B2B partners. The Shipping API Logic Server listens to Kafka and processes the message. Key Architectural Requirements: Self-Serve APIs and Shared Logic This sample illustrates some key architectural considerations: Requirement Poor Practice Good Practice Best Practice Ideal Ad Hoc Integration ETL APIs Self-Serve APIs Automated Self-Serve APIs Logic Logic in UI Reusable Logic Declarative Rules.. Extensible with Python Messages Kafka Kafka Logic Integration We'll further expand on these topics as we build the system, but we note some best practices: APIs should be self-serve, not requiring continuing server development. APIs avoid the nightly Extract, Transfer, and Load (ETL) overhead. Logic should be re-used over the UI and API transaction sources. Logic in UI controls is undesirable since it cannot be shared with APIs and messages. Using This Guide This guide was developed with API Logic Server, which is open-source and available here. The guide shows the highlights of creating the system. The complete Tutorial in the Appendix contains detailed instructions to create the entire running system. The information here is abbreviated for clarity. Development Overview This overview shows all the key codes and procedures to create the system above. We'll be using API Logic Server, which consists of a CLI plus a set of runtimes for automating APIs, logic, messaging, and an admin UI. It's an open-source Python project with a standard pip install. 1. ApiLogicServer Create: Instant Project The CLI command below creates an ApiLogicProject by reading your schema. The database is Northwind (Customer, Orders, Items, and Product), as shown in the Appendix. Note: the db_urlvalue is an abbreviation; you normally supply a SQLAlchemy URL. The sample NW SQLite database is included in ApiLogicServer for demonstration purposes. $ ApiLogicServer create --project_name=ApiLogicProject --db_url=nw- The created project is executable; it can be opened in an IDE and executed. One command has created meaningful elements of our system: an API for ad hoc integration and an Admin App. Let's examine these below. API: Ad Hoc Integration The system creates a JSON API with endpoints for each table, providing filtering, sorting, pagination, optimistic locking, and related data access. JSON: APIs are self-serve: consumers can select their attributes and related data, eliminating reliance on custom API development. In this sample, our self-serve API meets our Ad Hoc Integration needs and unblocks Custom UI development. Admin App: Order Entry UI The create command also creates an Admin App: multi-page, multi-table with automatic joins, ready for business user agile collaboration and back office data maintenance. This complements custom UIs you can create with the API. Multi-page navigation controls enable users to explore data and relationships. For example, they might click the first Customer and see their Orders and Items: We created an executable project with one command that completes our ad hoc integration with a self-serve API. 2. Customize: In Your IDE While API/UI automation is a great start, we now require Custom APIs, Logic, and Security. Such customizations are added to your IDE, leveraging all its services for code completion, debugging, etc. Let's examine these. Declare UI Customizations The admin app is not built with complex HTML and JavaScript. Instead, it is configured with the ui/admin/admin.yml, automatically created from your data model by the ApiLogicServer create command. You can customize this file in your IDE to control which fields are shown (including joins), hide/show conditions, help text, etc. This makes it convenient to use the Admin App to enter an Order and OrderDetails: Note the automation for automatic joins (Product Name, not ProductId) and lookups (select from a list of Products to obtain the foreign key). If we attempt to order too much Chai, the transaction properly fails due to the Check Credit logic described below. Check Credit Logic: Multi-Table Derivation and Constraint Rules, 40X More Concise. Such logic (multi-table derivations and constraints) is a significant portion of a system, typically nearly half. API Logic server provides spreadsheet-like rules that dramatically simplify and accelerate logic development. The five check credit rules below represent the same logic as 200 lines of traditional procedural code. Rules are 40X more concise than traditional code, as shown here. Rules are declared in Python and simplified with IDE code completion. Rules can be debugged using standard logging and the debugger: Rules operate by handling SQLAlchemy events, so they apply to all ORM access, whether by the API engine or your custom code. Once declared, you don't need to remember to call them, which promotes quality. The above rules prevented the too-big order with multi-table logic from copying the Product Price, computing the Amount, rolling it up to the AmountTotal and Balance, and checking the credit. These five rules also govern changing orders, deleting them, picking different parts, and about nine automated transactions. Implementing all this by hand would otherwise require about 200 lines of code. Rules are a unique and significant innovation, providing meaningful improvements over procedural logic: CHARACTERISTIC PROCEDURAL DECLARATIVE WHY IT MATTERS Reuse Not Automatic Automatic - all Use Cases 40X Code Reduction Invocation Passive - only if called Active - call not required Quality Ordering Manual Automatic Agile Maintenance Optimizations Manual Automatic Agile Design For more on the rules, click here. Declare Security: Customers See Only Their Own Row Declare row-level security using your IDE to edit logic/declare_security.sh, (see screenshot below). An automatically created admin app enables you to configure roles, users, and user roles. If users now log in as ALFKI (configured with role customer), they see only their customer row. Observe the console log at the bottom shows how the filter worked. Declarative row-level security ensures users see only the rows authorized for their roles. 3. Integrate: B2B and Shipping We now have a running system, an API, logic, security, and a UI. Now, we must integrate with the following: B2B partners: We'll create a B2B Custom Resource. OrderShipping: We add logic to Send an OrderShipping Message. B2B Custom Resource The self-serve API does not conform to the format required for a B2B partnership. We need to create a custom resource. You can create custom resources by editing customize_api.py using standard Python, Flask, and SQLAlchemy. A custom OrderB2B endpoint is shown below. The main task here is to map a B2B payload onto our logic-enabled SQLAlchemy rows. API Logic Server provides a declarative RowDictMapper class you can use as follows: Declare the row/dict mapping; see the OrderB2B class in the lower pane: Note the support for lookup so that partners can send ProductNames, not ProductIds. Create the custom API endpoint; see the upper pane: Add def OrderB2B to customize_api/py to create a new endpoint. Use the OrderB2B class to transform API request data to SQLAlchemy rows (dict_to_row). The automatic commit initiates the shared logic described above to check credit and reorder products. Our custom endpoint required under ten lines of code and the mapper configuration. Produce OrderShipping Message Successful orders must be sent to Shipping in a predesignated format. We could certainly POST an API, but Messaging (here, Kafka) provides significant advantages: Async: Our system will not be impacted if the Shipping system is down. Kafka will save the message and deliver it when Shipping is back up. Multi-cast: We can send a message that multiple systems (e.g., Accounting) can consume. The content of the message is a JSON string, just like an API. Just as you can customize APIs, you can complement rule-based logic using Python events: Declare the mapping; see the OrderShipping class in the right pane. This formats our Kafka message content in the format agreed upon with Shipping. Define an after_flush event, which invokes send_order_to_shipping. This is called by the logic engine, which passes the SQLAlchemy models.Order row. send_order_to_shipping uses OrderShipping.row_to_dict to map our SQLAlchemy order row to a dict and uses the Kafka producer to publish the message. Rule-based logic is customizable with Python, producing a Kafka message with 20 lines of code here. 4. Consume Messages The Shipping system illustrates how to consume messages. The sections below show how to create/start the shipping server create/start and use our IDE to add the consuming logic. Create/Start the Shipping Server This shipping database was created from AI. To simplify matters, API Logic Server has installed the shipping database automatically. We can, therefore, create the project from this database and start it: 1. Create the Shipping Project ApiLogicServer create --project_name=shipping --db_url=shipping 2. Start your IDE (e.g., code shipping) and establish your venv. 3. Start the Shipping Server: F5 (configured to use a different port). The core Shipping system was automated by ChatGPT and ApiLogicServer create. We add 15 lines of code to consume Kafka messages, as shown below. Consuming Logic To consume messages, we enable message consumption, configure a mapping, and provide a message handler as follows. 1. Enable Consumption Shipping is pre-configured to enable message consumption with a setting in config.py: KAFKA_CONSUMER = '{"bootstrap.servers": "localhost:9092", "group.id": "als-default-group1", "auto.offset.reset":"smallest"}' When the server is started, it invokes flask_consumer() (shown below). This is called the pre-supplied FlaskKafka, which handles the Kafka consumption (listening), thread management, and the handle annotation used below. This housekeeping task is pre-created automatically. FlaskKafka was inspired by the work of Nimrod (Kevin) Maina in this project. Many thanks! 2. Configure a Mapping As we did for our OrderB2B Custom Resource, we configured an OrderToShip mapping class to map the message onto our SQLAlchemy Order object. 3. Provide a Consumer Message Handler We provide the order_shipping handler in kafka_consumer.py: Annotate the topic handler method, providing the topic name. This is used by FlaskKafka to establish a Kafka listener Provide the topic handler code, leveraging the mapper noted above. It is called FlaskKafka per the method annotations. Test It You can use your IDE terminal window to simulate a business partner posting a B2BOrder. You can set breakpoints in the code described above to explore system operation. ApiLogicServer curl "'POST' 'http://localhost:5656/api/ServicesEndPoint/OrderB2B'" --data ' {"meta": {"args": {"order": { "AccountId": "ALFKI", "Surname": "Buchanan", "Given": "Steven", "Items": [ { "ProductName": "Chai", "QuantityOrdered": 1 }, { "ProductName": "Chang", "QuantityOrdered": 2 } ] } }}' Use Shipping's Admin App to verify the Order was processed. Summary These applications have demonstrated several types of application integration: Ad Hoc integration via self-serve APIs. Custom integration via custom APIs to support business agreements with B2B partners. Message-based integration to decouple internal systems by reducing dependencies that all systems must always be running. We have also illustrated several technologies noted in the ideal column: Requirement Poor Practice Good Practice Best Practice Ideal Ad Hoc Integration ETL APIs Self-Serve APIs Automated Creation of Self-Serve APIs Logic Logic in UI Reusable Logic Declarative Rules.. Extensible with Python Messages Kafka Kafka Logic Integration API Logic Server provides automation for the ideal practices noted above: 1. Creation: instant ad hoc API (and Admin UI) with the ApiLogicServer create command. 2. Declarative Rules: Security and multi-table logic reduce the backend half of your system by 40X. 3. Kafka Logic Integration Produce messages from logic events. Consume messages by extending kafka_consumer. Services, including: RowDictMapper to transform rows and dict. FlaskKafka for Kafka consumption, threading, and annotation invocation. 4. Standards-based Customization Standard packages: Python, Flask, SQLAlchemy, Kafka... Using standard IDEs. Creation, logic, and integration automation have enabled us to build two non-trivial systems with a remarkably small amount of code: Type Code Custom B2B API 10 lines Check Credit Logic 5 rules Row Level Security 1 security declaration Send Order to Shipping 20 lines Process Order in Shipping 30 lines Mapping configurationsto transform rows and dicts 45 lines Automation dramatically increases time to market, with standards-based customization using your IDE, Python, Flask, SQLAlchemy, and Kafka. For more information on API Logic Server, click here. Appendix Full Tutorial You can recreate this system and explore running code, including Kafka, click here. It should take 30-60 minutes, depending on whether you already have Python and an IDE installed. Sample Database The sample database is an SQLite version of Northwind, Customers, Order, OrderDetail, and Product. To see a database diagram, click here. This database is included when you pip install ApiLogicServer.
It has been over two months since the Kubernetes Gateway API made its v1.0 release, signifying graduation to the generally available status for some of its key APIs. I wrote about the Gateway API when it graduated to beta last year, but a year later, the question remains. Should you switch to the Gateway API from the Ingress API? My answer from last year was you shouldn't. And I had strong reasons. The Gateway API and its implementations were still in their infancy. The Ingress API, on the other hand, was stable and covered some primary use cases that might work for most users. For users requiring more capabilities, I suggested using the custom resources provided by the Ingress controllers by trading off portability (switching between different Ingress implementations). With the v1.0 release, this might change. The Gateway API is much more capable now, and its 20+ implementations are catching up quickly. So, if you are starting anew and choosing between the Ingress and the Gateway API, I suggest you pick the Gateway API if the API and the implementation you choose support all the features you want. What’s Wrong With the Ingress API? The Ingress API works very well, but only for a small subset of common use cases. To extend its capabilities, Ingress implementations started using custom annotations. For example, if you chose Nginx Ingress, you will use some of its dozens of annotations that are not portable if you decide to switch to another Ingress implementation like Apache APISIX. These implementation-specific annotations are also cumbersome to manage and defeat the purpose of managing Ingress in a Kubernetes-native way. Eventually, Ingress controller implementations started developing their CRDs to expose more features to Kubernetes users. These CRDs are specific to the Ingress controller. But if you can sacrifice portability and stick to one Ingress controller, the CRDs are easier to work with and offer more features. The Gateway API aims to solve this problem once and for all by providing the vendor agnosticism of the Ingress API and the flexibility of the CRDs. It is positioned very well to achieve this goal. In the long term, the Ingress API is not expected to receive any new features, and all efforts will be made to converge with the Gateway API. So, adopting the Ingress API can cause issues when you inadvertently hit limits with its capabilities. Obvious Benefits Expressive, extensible, and role-oriented are the key ideas that shaped the development of the Gateway API. Unlike the Ingress API, the Gateway API is a collection of multiple APIs (HTTPRoute, Gateway, GatewayClass, etc.), each catering to different organizational roles. For example, the application developers need to only care about the HTTPRoute resource, where they can define rules to route traffic. They can delegate the cluster-level details to an operator who manages the cluster and ensures that it meets the developers' needs using the Gateway resource. Adapted from gateway-api.sigs.k8s.io This role-oriented design of the API allows different people to use the cluster while maintaining control. The Gateway API is also much more capable than the Ingress API. Features that require annotations in the Ingress API are supported out-of-the-box in the Gateway API. An Official Extension Although the Gateway API is an official Kubernetes API, it is implemented as a set of CRDs. This is no different from using default Kubernetes resources. But you just have to install these CRDs like an official extension. The Ingress controller translates the Kubernetes resources to APISIX configuration implemented by API gateway. This allows for fast iteration compared to Kubernetes, which is slowly moving toward long-term stability. Will It Proliferate? As this famous XKCD comic reminds us frequently, standards tend to proliferate. A version of this was seen in the Ingress and Gateway APIs. It usually goes like this: A standard emerges to unify different projects/their standards (Kubernetes Ingress API). The unified standard has limitations the implementors want to overcome (Ingress API was limited). Implementations diverge from the standard because of these limitations (Custom CRDs, annotations). Each implementation now has its standard (non-portable CRDs, annotations). A new standard emerges to unify these different standards (Kubernetes Gateway API). It is reasonable to think that the Gateway API might not be the end game here. But I believe it has every chance of being the standard for routing in Kubernetes. Again, I have my strong reasons. Broad adoption is critical to prevent standard proliferation as there are fewer incentives for the implementations to work on a different standard. The Gateway API already has more than 25 implementations. An implementation can conform to the Gateway API on different levels: Core: All implementations are expected to conform to these. Extended: These might only be available in some implementations but are standard APIs. Implementation-specific: Specific to implementations but added through standard extension points. A niche feature can move from implementation-specific to extended to the core as more implementations support these features. i.e., the API allows room for custom extensions while ensuring it follows the standard. The Service Mesh Interface (SMI) project was a similar attempt to standardize configuring service meshes in Kubernetes. However, the project received little traction after the initial involvement of the service mesh projects and slowly died out. SMI did not support many common denominator features that users expected in a service mesh. It also did not move fast enough to support these features. Eventually, service mesh implementations fell behind in conforming to SMI (I used to work closely with SMI under the CNCF TAG Network on a project that reported SMI conformance). These are universal reasons, but the project is now being resurrected through the Gateway API. The Gateway API for Mesh Management and Administration (GAMMA) initiative aims to extend the Gateway API to work with service meshes. The SMI project recently merged with the GAMMA initiative, which is excellent for the Gateway API. Istio, undoubtedly the most popular service mesh, also announced that the Gateway API will be the default API to manage Istio in the future. Such adoptions prevent proliferation. Migration Guide The Gateway API documentation has a comprehensive guide on migrating your Ingress resources to Gateway resources. Instead of restating it, let's try using the ingress2gateway tool to convert our Ingress resources to corresponding Gateway API resources. You can download and install the binary for your operating system directly from the releases page. Let's take a simple Ingress resource: YAML apiVersion: networking.k8s.io/v1 kind: Ingress metadata: name: httpbin-route spec: ingressClassName: apisix rules: - host: local.httpbin.org http: paths: - backend: service: name: httpbin port: number: 80 path: / pathType: Prefix This will route all traffic with the provided host address to the httpbin service. To convert it to the Gateway API resource, we can run: Shell ingress2gateway print --input_file=ingress.yaml This Gateway API resource will be as shown below: YAML apiVersion: gateway.networking.k8s.io/v1alpha2 kind: HTTPRoute metadata: name: httpbin-route spec: hostnames: - local.httpbin.org rules: - matches: - path: type: PathPrefix value: / backendRefs: - name: httpbin port: 80 Viable Alternatives There are other viable alternatives for configuring gateways in Kubernetes. In Apache APISIX, you can deploy it in standalone mode and define route configurations in a YAML file. You can update this YAML file through traditional workflows, and it can be pretty helpful in scenarios where managing the gateway configuration via the Kubernetes API is not required. Implementation-specific custom CRDs are also viable alternatives if you don't plan to switch to a different solution or if your configuration is small enough to migrate easily. In any case, the Gateway API is here to stay.
If you’re not yet familiar with the open-source pgvector extension for PostgreSQL, now’s the time to do so. The tool is extremely helpful for searching text data fast without needing a specialized database to store embeddings. Embeddings represent word similarity and are stored as vectors (a list of numbers). For example, the words “tree” and “bush” are related more closely than “tree” and “automobile.” The open-source pgvector tool makes it possible to search for closely related vectors and find text with the same semantic meaning. This is a major advance for text-based data, and an especially valuable tool for building Large Language Models (LLMs)... and who isn’t right now? By turning PostgreSQL into a high-performance vector store with distance-based embedding search capabilities, pgvector allows users to explore vast textual data easily. This also enables exact nearest neighbor search and approximate nearest neighbor search using L2 (or Euclidian) distance, inner product, and cosine distance. Cosine distance is recommended by OpenAI for capturing semantic similarities efficiently. Using Embeddings in Retrieval Augmented Generation (RAG) and LLMs Embeddings can play a valuable role in the Retrieval Augmented Generation (RAG) process, which is used to fine-tune LLMs on new knowledge. The process includes retrieving relevant information from an external source, transforming it into an LLM digestible format, and then feeding it to the LLM to generate text output. Let’s put an example to it. Searching documentation for answers to technical problems is something I’d bet anyone here has wasted countless hours on. For this example below, using documentation as the source, you can generate embeddings to store in PostgreSQL. When a user queries that documentation, the embeddings make it possible to represent the words in a query as vector numbers, perform a similarity search, and retrieve relevant pieces of the documentation from the database. The user’s query and retrieved documentation are both passed to the LLM, which accurately delivers relevant documentation and sources that answer the query. We tested out pgvector and embeddings using our own documentation at Instaclustr. Here are some example user search phrases to demonstrate how embeddings will plot them relative to one another: “Configure hard drive failure setting in Apache Cassandra” “Change storage settings in Redis” “Enterprise pricing for a 2-year commitment” “Raise a support ticket” “Connect to PostgreSQL using WebSockets” Embeddings plot the first two phases nearest each other, even though they include none of the same words. The LLM Context Window Each LLM has a context window: the number of tokens it can process at once. This can be a challenge, in that models with a limited context window can falter with large inputs, but models trained with large context windows (100,000 tokens, or enough to use a full book in a prompt) suffer from latency and must store that full context in memory. The goal is to use the smallest possible context window that generates useful answers. Embeddings help by making it possible to provide the LLM with only data recognized as relevant so that even an LLM with a tight context window isn’t overwhelmed. Feeding the Embedding Model With LangChain The model that generates embeddings — OpenAI’s text-embedding-ada-002 — has a context window of its own. That makes it essential to break documentation into chunks so this embedding model can digest more easily. The LangChain Python framework offers a solution. An LLM able to answer documentation queries needs these tasks completed first: Document loading: LangChain makes it simple to scrape documentation pages, with the ability to load diverse document formats from a range of locations. Document transformation: Segmenting large documents into smaller digestible chunks enables retrieval of pertinent document sections. Embedding generation: Calculate embeddings for the chunked documentation using OpenAI’s embedding model. Data storing: Store embeddings and original content in PostgreSQL. This process yields the semantic index of documentation we’re after. An Example User Query Workflow Now consider this sample workflow for a user query (sticking with our documentation as the example tested). First, a user submits the question: “How do I create a Redis cluster using Terraform?” OpenAI’s embeddings API calculates the question’s embeddings. The system then queries the semantic index in PostgreSQL using cosine similarity, asking for the original content closest to the embeddings of the user’s question. Finally, the system grabs the original content returned in the vector search, concatenates it together, and includes it in a specially crafted prompt with the user’s original question. Implementing pgvector and a User Interface Now let’s see how we put pgvector into action. First, we enabled the pgvector extension in our PostgreSQL database, and created a table for storing all documents and their embeddings: Python CREATE EXTENSION vector; CREATE TABLE insta_documentation (id bigserial PRIMARY KEY, title, content, url, embedding vector(3)); The following Python code scrapes the documentation, uses Beautiful Soup to extract main text parts such as title and content, and stores them and the URL in the PostgreSQL table: Python urls = [...] def init_connection(): return psycopg2.connect(**st.secrets["postgres"]) def extract_info(url): hdr = {'User-Agent': 'Mozilla/5.0'} req = Request(url,headers=hdr) response = urlopen(req) soup = BeautifulSoup(response, 'html.parser') title = soup.find('title').text middle_section = soup.find('div', class_='documentation-middle').contents # middle section consists of header, content and instaclustr banner and back and forth links - we want only the first two content = str(middle_section[0]) + str(middle_section[1]) return title, content, url conn = init_connection() cursor = conn.cursor() for url in urls: page_content = extract_info(url) postgres_insert_query = """ INSERT INTO insta_documentation (title, content, url) VALUES (%s, %s, %s)""" cursor.execute(postgres_insert_query, page_content) conn.commit() if conn: cursor.close() conn.close() Next, we loaded the documentation pages from the database, divided them into chunks, and created and stored the crucial embeddings. Python def init_connection(): return psycopg2.connect(**st.secrets["postgres"]) conn = init_connection() cursor = conn.cursor() # Define and execute query to the insta_documentation table, limiting to 10 results for testing (creating embeddings through the OpenAI API can get costly when dealing with a huge amount of data) postgres_query = """ SELECT title, content, url FROM insta_documentation LIMIT 10""" cursor.execute(postgres_query) results = cursor.fetchall() conn.commit() # Load results into pandas DataFrame for easier manipulation df = pd.DataFrame(results, columns=['title', 'content', 'url']) # Break down content text which exceed max input token limit into smaller chunk documents # Define text splitter html_splitter = RecursiveCharacterTextSplitter.from_language(language=Language.HTML, chunk_size=1000, chunk_overlap=100) # We need to initialize our embeddings model embeddings = OpenAIEmbeddings(model="text-embedding-ada-002") docs = [] for i in range(len(df.index)): # Create document with metadata for each content chunk docs = docs + html_splitter.create_documents([df['content'][i]], metadatas=[{"title": df['title'][i], "url": df['url'][i]}]) # Create pgvector dataset db = Pgvector.from_documents( embedding=embeddings, documents=docs, collection_name=COLLECTION_NAME, connection_string=CONNECTION_STRING, distance_strategy=DistanceStrategy.COSINE, ) Lastly, the retriever found the correct information to answer a given query. In our test example, we searched our documentation to learn how to sign up for an account: Python query = st.text_input('Your question', placeholder='How can I sign up for an Instaclustr console account?') retriever = store.as_retriever(search_kwargs={"k": 3}) qa = RetrievalQA.from_chain_type( llm=OpenAI(), chain_type="stuff", retriever=retriever, return_source_documents=True, verbose=True, ) result = qa({"query": query}) source_documents = result["source_documents"] document_page_content = [document.page_content for document in source_documents] document_metadata = [document.metadata for document in source_documents] Using Streamlit, a powerful tool for building interactive Python interfaces, we built this interface to test the system and view the successful query results: Data Retrieval With Transformative Efficiency Harnessing PostgreSQL and the open-source pgvector project empowers users to leverage natural language queries to answer questions immediately, with no need to comb through irrelevant data. The result: super accurate, performant, and efficient LLMs, groundbreaking textual capabilities, and meaningful time saved!
In our digital age, the role of APIs (Application Programming Interfaces) in business is more crucial than ever. These APIs allow companies to be innovative, grow quickly, and adapt their services. But, as much as APIs are vital, they also bring a new set of challenges, especially in security. This is why the concept of "Zero Trust" in managing API security is gaining momentum, representing a fundamental change in how companies safeguard their digital assets. Why Is Enhanced Security Necessary for APIs? APIs are the unsung heroes of the digital world, connecting different software and services. However, with their widespread use comes an increased risk of security breaches. Traditional security methods, which mainly focus on protecting the network's perimeter, are no longer effective. Cyber threats today can come from anywhere, even from within an organization. Hence, a new approach is needed, and Zero Trust fits this need perfectly. It is a model where trust is not a given; it has to be earned and verified, regardless of whether a request comes from inside or outside the network. Understanding Zero Trust in Simple Terms Zero Trust is not just a fancy term; it is a shift in how we think about security. In a Zero Trust model, every single request for access is checked thoroughly. It's like a diligent security guard who checks everyone's ID each time they enter, no matter how well he knows them. How To Implement Zero Trust in API Management Start with a Clear Assessment: The first step is understanding your current API setup. Know where your sensitive data is and who has access to it. Define Access Roles and Policies: Carefully determine who can access what. Not everyone in the organization needs access to all parts of the API. Choose the Right Technology Tools: Technologies like IAM (Identity and Access Management) and API gateways are crucial. They act like digital gatekeepers, overseeing who gets access to your APIs. Incorporate Security from the Beginning: When designing APIs, make security a core element, not an afterthought. Implement Strong Authentication Measures: This might include multi-factor authentication, adding an extra layer of security. Automate Security Enforcement: Use technology to consistently apply security policies without manual intervention. Stay Vigilant and Update Regularly: The digital landscape is always changing, so it is important to keep your security measures up-to-date. Advantages of Zero Trust Robust Security: Zero Trust significantly strengthens your defense against data breaches and cyber-attacks. Trust and Compliance: It builds trust among customers and partners and helps comply with various regulatory standards. Effective Risk Management: This proactive approach enables organizations to identify and mitigate risks before they escalate. Embracing the Challenges Adopting Zero Trust is not straightforward. It involves a shift in the organization's culture towards security. It requires investment in appropriate technology and an ongoing commitment to adapt and update security strategies. In summary, Zero Trust in API management is not just a security strategy; it is a comprehensive approach to ensuring continuous vigilance and adaptation in a world where digital threats constantly evolve. It's about creating an environment where security is paramount and everyone is part of a culture that prioritizes keeping data safe. Adopting Zero Trust means making a strong commitment to safeguarding your digital ecosystem in an increasingly interconnected world.
The pursuit of speed and agility in software development has given rise to methodologies and practices that transcend traditional boundaries. Continuous testing, a cornerstone of modern DevOps practices, has evolved to meet the demands of accelerated software delivery. In this article, we'll explore the latest advancements in continuous testing, focusing on how it intersects with microservices and serverless architectures. I. The Foundation of Continuous Testing Continuous testing is a practice that emphasizes the need for testing at every stage of the software development lifecycle. From unit tests to integration tests and beyond, this approach aims to detect and rectify defects as early as possible, ensuring a high level of software quality. It extends beyond mere bug detection and it encapsulates a holistic approach. While unit tests can scrutinize individual components, integration tests can evaluate the collaboration between diverse modules. The practice allows not only the minimization of defects but also the robustness of the entire system. Its significance lies in fostering a continuous loop of refinement, where feedback from tests informs and enhances subsequent development cycles, creating a culture of continual improvement. II. Microservices: Decoding the Complexity Microservices architecture has become a dominant force in modern application development, breaking down monolithic applications into smaller, independent services. This signifies a departure from monolithic applications, introducing a paradigm shift in how software is developed and deployed. While this architecture offers scalability and flexibility, it comes with the challenge of managing and testing a multitude of distributed services. Microservices' complexity demands a nuanced testing strategy that acknowledges their independent functionalities and interconnected nature. Decomposed Testing Strategies Decomposed testing strategies are key to effective microservices testing. This approach advocates for the examination of each microservice in isolation. It involves a rigorous process of testing individual services to ensure their functionality meets specifications, followed by comprehensive integration testing. This methodical approach not only identifies defects at an early stage but also guarantees seamless communication between services, aligning with the modular nature of microservices. It fosters a testing ecosystem where each microservice is considered an independent unit, contributing to the overall reliability of the system. A sample of testing strategies that fall in this category include, but are not limited to: Unit Testing for Microservices Unit testing may be used to verify the correctness of individual microservices. If you have a microservice responsible for user authentication, for example, unit tests would check whether the authentication logic works correctly, handles different inputs, and responds appropriately to valid and invalid authentication attempts. Component Testing for Microservices Component testing may be used to test the functionality of a group of related microservices or components. In an e-commerce system, for example, you might have microservices for product cataloging, inventory management, and order processing. Component testing would involve verifying that these microservices work together seamlessly to enable processes like placing an order, checking inventory availability, and updating the product catalog. Contract Testing This is used to ensure that the contracts between microservices are honored. If microservice A relies on data from microservice B, contract tests would verify that microservice A can correctly consume the data provided by microservice B. This may ensure that changes to microservice B don't inadvertently break the expectations of microservice A. Performance Testing for Microservices Performance tests on a microservice could involve evaluating its response time, scalability, and resource utilization under various loads. This helps identify potential performance bottlenecks early in the development process. Security Testing for Microservices Security testing for a microservice might involve checking for vulnerabilities, ensuring proper authentication and authorization mechanisms are in place, and verifying that sensitive data is handled securely. Fault Injection Testing This is to assess the resilience of each microservice to failures. You could intentionally inject faults, such as network latency or service unavailability, into a microservice and observe how it responds. This helps ensure that microservices can gracefully handle unexpected failures. Isolation Testing Isolation testing verifies that a microservice operates independently of others. Isolation tests may involve testing a microservice with its dependencies mocked or stubbed. This ensures that the microservice can function in isolation and doesn't have hidden dependencies that could cause issues in a real-world environment. Service Virtualization Service virtualization is indispensable to microservices. It addresses the challenge of isolating and testing microservices by allowing teams to simulate their behavior in controlled environments. Service virtualization empowers development and testing teams to create replicas of microservices, facilitating isolated testing without dependencies on the entire system. This approach not only accelerates testing cycles but also enhances the accuracy of results by replicating real-world scenarios. It may become an enabler, ensuring thorough testing without compromising the agility required in the microservices ecosystem. API Testing Microservices heavily rely on APIs for seamless communication. Robust API testing becomes paramount in validating the reliability and functionality of these crucial interfaces. An approach to API testing involves scrutinizing each API endpoint's response to various inputs and edge cases. This examination may ensure that microservices can effectively communicate and exchange data as intended. API testing is not merely a validation of endpoints; it is a verification of the entire communication framework, forming a foundational layer of confidence in the microservices architecture. III. Serverless Computing: Revolutionizing Deployment Serverless computing takes the abstraction of infrastructure to unprecedented levels, allowing developers to focus solely on code without managing underlying servers. While promising unparalleled scalability and cost efficiency, it introduces a paradigm shift in testing methodologies that demands a new approach to ensure the reliability of serverless applications. Event-Driven Testing Serverless architectures are often event-driven, responding to triggers and stimuli. Event-driven testing becomes a cornerstone in validating the flawless execution of functions triggered by events. One approach involves not only scrutinizing the function's response to specific events but also assessing its adaptability to dynamic and unforeseen triggers. Event-driven testing ensures that serverless applications respond accurately and reliably to diverse events, fortifying the application against potential discrepancies. This approach could be pivotal in maintaining the responsiveness and integrity of serverless functions in an event-centric environment. Cold Start Challenges Testing the performance of serverless functions, especially during cold starts, emerges as a critical consideration in serverless computing. One approach to addressing cold start challenges involves continuous performance testing. This may help serverless functions perform optimally even when initiated from a dormant state, identifying and addressing latency issues promptly. By proactively tackling cold start challenges, development teams may confidently allow for a seamless user experience, regardless of the serverless function's initialization state. Third-Party Services Integration Serverless applications often rely on seamless integration with third-party services. Ensuring compatibility and robustness in these integrations becomes a crucial aspect of continuous testing for serverless architectures. One approach involves rigorous testing of the interactions between serverless functions and third-party services, verifying that data exchanges occur flawlessly. By addressing potential compatibility issues and ensuring the resilience of these integrations, development teams may fortify the serverless application's reliability and stability. IV. Tools and Technologies The evolution of continuous testing can be complemented by a suite of tools and technologies designed to streamline testing processes in microservices and serverless architectures. These tools not only facilitate testing but also enhance the overall efficiency and effectiveness of the testing lifecycle. Testing Frameworks for Microservices Tools like JUnit, TestNG, Spock, Pytest, and Behave are a sample of tools that can be useful in the comprehensive testing of microservices. These frameworks support unit tests, integration tests, and end-to-end tests. Contract tests may further validate that each microservice adheres to specified interfaces and communication protocols. Serverless Testing Tools Frameworks such as AWS SAM (Serverless Application Model), Serverless Framework, AWS Lambda Test, Azure Functions Core Tools, and Serverless Offline are all tools that help you develop, test, and deploy serverless applications. However, they have different features and purposes. AWS SAM is a tool that makes it easier to develop and deploy serverless applications on AWS. It provides a YAML-based syntax for defining your serverless applications, and it integrates with AWS CloudFormation to deploy your applications. Additionally, AWS SAM provides a local development environment that lets you test your applications before deploying them to AWS. Serverless Framework is a tool that supports serverless deployments on multiple cloud providers, including AWS, Azure, and Google Cloud Platform (GCP). It provides a CLI interface for creating, updating, and deploying serverless applications. Additionally, Serverless Framework provides a plugin system that lets you extend its functionality with third-party extensions. AWS Lambda Test is a tool that lets you test your AWS Lambda functions locally. It provides a simulated AWS Lambda environment that you can use to run your functions and debug errors. Additionally, AWS Lambda Test can generate test cases for your Lambda functions, which can help you improve your code coverage. Azure Functions Core Tools is a tool that lets you develop and test Azure Functions locally. It provides a CLI interface for creating, updating, and running Azure Functions. Additionally, Azure Functions Core Tools can generate test cases for your Azure Functions, which can help you improve your code coverage. Serverless Offline is a tool that lets you test serverless applications locally, regardless of the cloud provider that you are using. It provides a simulated cloud environment that you can use to run your serverless applications and debug errors. Additionally, Serverless Offline can generate test cases for your serverless applications, which can help you improve your code coverage. Here is a table that summarizes the key differences between the five tools: Feature AWS SAM Serverless Framework AWS Lambda Test Azure Functions Core Tools Serverless Offline Cloud provider support AWS AWS, Azure, GCP AWS Azure Multi-cloud Deployment YAML-based syntax integrates with AWS CloudFormation CLI interface Not supported CLI interface Not supported Local development environment Yes Yes Yes Yes Yes Plugin system No Yes No No No Test case generation Yes No Yes Yes Yes CI/CD Integration Continuous testing seamlessly integrates with CI/CD pipelines, forming a robust and automated testing process. Tools such as Jenkins, GitLab CI, and Travis CI orchestrate the entire testing workflow, ensuring that each code change undergoes rigorous testing before deployment. The integration of continuous testing with CI/CD pipelines provides a mechanism for maintaining software quality while achieving the speed demanded by today's digital economy. V. Wrapping Up Continuous testing is a central element in the process of delivering software quickly and reliably. It's an essential part that holds everything together since it involves consistently checking the software for issues and bugs throughout its development. As microservices and serverless architectures continue to reshape the software landscape, the role of continuous testing becomes even more pronounced. Embracing the challenges posed by these innovative architectures and leveraging the latest tools and methodologies may empower development teams to deliver high-quality software at the speed demanded by today's digital economy.
Our Excel spreadsheets hold a lot of valuable data in their dozens, hundreds, or even thousands of cells and rows. With that much clean, formatted digital data at our disposal, it’s up to us to find programmatic methods for extracting and sharing that data among other important documents in our file ecosystem. Thankfully, Microsoft made that extremely easy to do when they switched their file representation standard over to OpenXML more than 15 years ago. This open-source XML-based approach drastically improved the accessibility of all Office document contents by basing their structure on well-known technologies – namely Zip and XML – which most software developers intimately understand. Before that, Excel (XLS) files were stored in a binary file format known as BIFF (Binary Interchange File Format), and other proprietary binary formats were used to represent additional Office files like Word (DOC). This change to an open document standard made it possible for developers to build applications that could interact directly with Office documents in meaningful ways. To get information about the structure of a particular Excel workbook, for example, a developer could write code to access xl/workbook.xml in the XLSX file structure and get all the workbook metadata they need. Similarly, to get specific sheet data, they could access xl/worksheets/(sheetname).xml, knowing that each cell and value with that sheet will be represented by simple <c> and <v> elements with all their relevant data nested within. This is a bit of an oversimplification, but it serves to point out the ease of navigating a series of zipped XML file paths. Given the global popularity of Excel files, building (or simply expanding) applications to load, manipulate, and extract content from XLSX was a no-brainer. There are dozens of examples of modern applications that can seamlessly load & manipulate XLSX files, and many even provide the option to export files in XLSX format. When we set out to build our applications to interact with Excel documents, we have several options at our disposal. We can elect to write our code to sift through OpenXML document formatting, or we can download a specialized programming library, or we can alternatively call a specially designed web API to take care of a specific document interaction on our behalf. The former two options can help us keep our code localized, but they’ll chew up a good amount of keyboard time and prove a little more costly to run. With the latter option, we can offload our coding and processing overhead to an external service, reaping all the benefits with a fraction of the hassle. Perhaps most beneficially, we can use APIs to save time and rapidly get our application prototypes off the ground. Demonstration In the remainder of this article, I’ll quickly demonstrate two free-to-use web APIs that allow us to retrieve content from specific cells in our XLSX spreadsheets in slightly different ways. Ready-to-run Java code is available below to make structuring our calls straightforward. Both APIs will return information about our target cell, including the cell path, cell text value, cell identifier, cell style index, and the formula (if any) used within that cell. With the information in our response object, we can subsequently ask our applications to share data between spreadsheets and other open standard files for myriad purposes. Conveniently, both API requests can be authorized with the same free API key. It’s also important to note that both APIs process file data in memory and release that data upon completion of the request. This makes both requests fast and extremely secure. The first of these two API solutions will locate the data we want using the row index and cell index in our request. The second solution will instead use the cell identifier (i.e., A1, B1, C1, etc.) for the same purpose. While cell index and cell identifier are often regarded interchangeably (both locate a specific cell in a specific location within an Excel worksheet), using the cell index can make it easier for our application to adapt dynamically to any changes within our document, while the cell identifier will always remain static. To use these APIs, we’ll start by installing the SDK with Maven. We can first add a reference to the repository in pom.xml: XML <repositories> <repository> <id>jitpack.io</id> <url>https://jitpack.io</url> </repository> </repositories> We can then add a reference to the dependency in pom.xml: XML <dependencies> <dependency> <groupId>com.github.Cloudmersive</groupId> <artifactId>Cloudmersive.APIClient.Java</artifactId> <version>v4.25</version> </dependency> </dependencies> With installation out of the way, we can structure our request parameters and use ready-to-run Java code examples to make our API calls. To retrieve cell data using the row index and cell index, we can format our request parameters like the application/JSON example below: JSON { "InputFileBytes": "string", "InputFileUrl": "string", "WorksheetToQuery": { "Path": "string", "WorksheetName": "string" }, "RowIndex": 0, "CellIndex": 0 } And we can use the below code to call the API once our parameters are set: Java // Import classes: //import com.cloudmersive.client.invoker.ApiClient; //import com.cloudmersive.client.invoker.ApiException; //import com.cloudmersive.client.invoker.Configuration; //import com.cloudmersive.client.invoker.auth.*; //import com.cloudmersive.client.EditDocumentApi; ApiClient defaultClient = Configuration.getDefaultApiClient(); // Configure API key authorization: Apikey ApiKeyAuth Apikey = (ApiKeyAuth) defaultClient.getAuthentication("Apikey"); Apikey.setApiKey("YOUR API KEY"); // Uncomment the following line to set a prefix for the API key, e.g. "Token" (defaults to null) //Apikey.setApiKeyPrefix("Token"); EditDocumentApi apiInstance = new EditDocumentApi(); GetXlsxCellRequest input = new GetXlsxCellRequest(); // GetXlsxCellRequest | Document input request try { GetXlsxCellResponse result = apiInstance.editDocumentXlsxGetCellByIndex(input); System.out.println(result); } catch (ApiException e) { System.err.println("Exception when calling EditDocumentApi#editDocumentXlsxGetCellByIndex"); e.printStackTrace(); } To retrieve cell data using the cell identifier, we can format our request parameters like the application/JSON example below: JSON { "InputFileBytes": "string", "InputFileUrl": "string", "WorksheetToQuery": { "Path": "string", "WorksheetName": "string" }, "CellIdentifier": "string" } We can use the final code examples below to structure our API call once our parameters are set: Java // Import classes: //import com.cloudmersive.client.invoker.ApiClient; //import com.cloudmersive.client.invoker.ApiException; //import com.cloudmersive.client.invoker.Configuration; //import com.cloudmersive.client.invoker.auth.*; //import com.cloudmersive.client.EditDocumentApi; ApiClient defaultClient = Configuration.getDefaultApiClient(); // Configure API key authorization: Apikey ApiKeyAuth Apikey = (ApiKeyAuth) defaultClient.getAuthentication("Apikey"); Apikey.setApiKey("YOUR API KEY"); // Uncomment the following line to set a prefix for the API key, e.g. "Token" (defaults to null) //Apikey.setApiKeyPrefix("Token"); EditDocumentApi apiInstance = new EditDocumentApi(); GetXlsxCellByIdentifierRequest input = new GetXlsxCellByIdentifierRequest(); // GetXlsxCellByIdentifierRequest | Document input request try { GetXlsxCellByIdentifierResponse result = apiInstance.editDocumentXlsxGetCellByIdentifier(input); System.out.println(result); } catch (ApiException e) { System.err.println("Exception when calling EditDocumentApi#editDocumentXlsxGetCellByIdentifier"); e.printStackTrace(); } That’s all the code we’ll need. With utility APIs at our disposal, we’ll have our projects up and running in no time.
John Vester
Staff Engineer,
Marqeta @JohnJVester
Colin Domoney
Chief Technology Evangelist,
42Crunch
Saurabh Dashora
Founder,
ProgressiveCoder