Sr. Content Developer at Microsoft, working remotely in PA, TechBash conference organizer, former Microsoft MVP, Husband, Dad and Geek.
162916 stories
·
33 followers

Better HTTP/1 connection reuse

1 Share

The Go Blog

Better HTTP/1 connection reuse

Nicholas Husin and Damien Neil
14 October 2026

Take a look at the following function:

func getStatus(url string) (int, error) {
    resp, err := http.Get(url)
    if err != nil {
        return 0, err
    }
    defer resp.Body.Close()
    return resp.StatusCode, nil
}

It sends a GET request to url and returns the HTTP status code.

Using HTTP/1 (GODEBUG=http2client=0), calling this function 100 times against index.golang.org takes ~710 milliseconds on Go 1.26 on my machine. On Go 1.27, the same program takes ~150 milliseconds.

That is almost 5 times faster! How is such a drastic performance difference possible?

The cost of establishing HTTP connections

To understand the difference, consider what happens on the network when we make an HTTP request.

Go’s net/http package supports HTTP/1.0, HTTP/1.1, and HTTP/2. All three run over TCP, and usually over TLS when talking to HTTPS endpoints. Making a request over a new connection looks something like this:

  1. TCP handshake (1 RTT): the client sends a SYN packet, the server replies with SYN-ACK, and the client replies with ACK. Because the client can send the first TLS message alongside that final ACK, establishing the TCP connection takes 1 round-trip time (RTT).
  2. TLS handshake (2 RTT): next, a TLS handshake exchanges encryption keys and verifies the server’s identity; using TLS 1.2, this takes 2 RTT.
  3. HTTP request: with the keys exchanged, HTTP requests can finally be sent after a total of 3 RTT.

These round trips (plus some CPU time spent on TLS handshakes) add up quickly. In cross-region communications, one-way latencies often reach or exceed the 50 ms (100 ms RTT) shown in the diagram above. Compared to the 300 ms spent just setting up the connection, the HTTP request itself can be cheap to serve, taking only a little over 1 RTT when bandwidth is plentiful or the HTTP response is small.

Even in more favorable conditions, opening a new connection can still be expensive. Our opening benchmark queries index.golang.org, which is distributed worldwide and supports TLS 1.3. This means that latency is relatively low, and the TLS handshake can actually finish in just 1 RTT. Even so, the performance difference is still very noticeable.

Connection reuse

To avoid paying this setup cost on every request, HTTP clients and servers try to reuse existing connections across multiple requests when possible. The mechanics of this differ based on the HTTP version:

  • HTTP/1.0: while not part of the official specification, clients and servers often opt into connection reuse by sending a Connection: keep-alive header. Reuse continues until either endpoint omits the header or sends a Connection: close header.
  • HTTP/1.1: connections are persistent by default, and can be reused for subsequent requests and responses unless either side declares otherwise by sending a Connection: close header.
  • HTTP/2: multiple requests and responses can be multiplexed over a single TCP connection.

When connection reuse fails

In HTTP/1 (both HTTP/1.0 and HTTP/1.1), multiple requests and responses on the same connection are sent sequentially on the wire. If two GET requests are sent to index.golang.org over the same TCP connection, the responses arrive as a single stream of bytes:

As shown in the diagram, net/http.Get reads and parses the status line and headers from the byte stream to populate net/http.Response, then returns immediately. The response body remains unread on the connection and is consumed only as the caller reads from net/http.Response.Body.

Because responses are sent back-to-back on the same stream, net/http can only find the start of the next response after the current response body has been read to the very end (as determined by the Content-Length or Transfer-Encoding header).

Now look back at our example program, where we never read resp.Body:

func getStatus(url string) (int, error) {
    resp, err := http.Get(url)
    if err != nil {
        return 0, err
    }
    defer resp.Body.Close()
    return resp.StatusCode, nil
}

Through Go 1.26, when resp.Body.Close is called, net/http sees that there is still an unread response body. Realizing that it cannot reuse the connection, it abandons the existing connection and creates new connections for its next requests, causing it to repeatedly incur the cost of establishing new connections.

(HTTP/2 does not have this problem. Because it multiplexes streams of frames over a single connection rather than sending responses strictly back-to-back, an unread response body on one stream does not block other requests from sharing the connection.)

Where failed connection reuse hurts

This subtlety of HTTP/1 connection reuse can cause silent performance degradation in seemingly innocuous code.

As in our opening example, a client might only care about an API response’s status code. However, the API endpoint it is talking to might send bodies that provide more detailed but ultimately unused information, such as {"success": true, "message": "update processed"}. This will unfortunately cause connection reuse to fail.

Even programs that do read the response body can run into this issue if they stop just short of the end of the body. For example, encoding/json.Decoder stops reading as soon as it sees the closing } of a JSON object. If a server sends a JSON response terminated with a newline, decoding the JSON object will leave the newline still unread. The connection cannot be reused for a new request until that newline is consumed. Thus, a healthy connection might end up being abandoned, even though it was only a single byte away from being reusable.

The manual solution

The unread body problem has been a long-standing sharp edge. Over the years, many Go developers came up with their own workarounds. Most commonly, this is done by draining the response body prior to closing it:

func getStatus(url string) (int, error) {
    resp, err := http.Get(url)
    if err != nil {
        return 0, err
    }
    drainAndCloseBody(resp.Body)
    return resp.StatusCode, nil
}

func drainAndCloseBody(body io.ReadCloser) {
    io.Copy(io.Discard, body) // Drain the response body before closing it.
    body.Close()
}

While this works for small responses, if the unread body is large, establishing a new TCP connection can be faster than downloading and discarding the rest of the body. A better approach is to cap the number of bytes we are willing to drain:

func drainAndCloseBody(body io.ReadCloser) {
    io.CopyN(io.Discard, body, 256<<10) // Only drain up to 256 KB.
    body.Close()
}

However, a server might send a response body that trickles in slowly, so we also need to cap the time we spend draining:

func drainAndCloseBody(body io.ReadCloser) {
    drainDone := make(chan struct{}, 1)
    go func() {
        io.CopyN(io.Discard, body, 256<<10) // Only drain up to 256 KB.
        drainDone <- struct{}{}
    }()
    // Only spend at most 50 ms trying to drain the response body.
    select {
    case <-drainDone:
    case <-time.After(50 * time.Millisecond):
        // If we reach the 50 ms timeout, body.Close will run and interrupt the
        // io.CopyN, preventing goroutine leaks.
    }
    body.Close()
}

Of course, we also do not want to block for up to 50 milliseconds when closing a response body. Instead, we can drain the body asynchronously in the background and let it race against subsequent requests:

func drainAndCloseBody(body io.ReadCloser) {
    drainDone := make(chan struct{}, 1)
    go func() {
        io.CopyN(io.Discard, body, 256<<10) // Only drain up to 256 KB.
        drainDone <- struct{}{}
    }()
    go func() {
        // Only spend at most 50 ms trying to drain the response body.
        select {
        case <-drainDone:
            // If the drain can finish before a new request is sent, the
            // connection will be reused; otherwise, a new connection is
            // created, and the current connection will be put in a pool of
            // idle connections, potentially being reused for another future
            // request.
        case <-time.After(50 * time.Millisecond):
            // If we reach the 50 ms timeout, body.Close will run and interrupt
            // the io.CopyN, preventing goroutine leaks.
        }
        body.Close()
    }()
}

As we can see, draining response bodies safely is surprisingly tricky and cumbersome. Worse, as the practice of draining response bodies spread, some nuances were often lost. This has led to misunderstandings such as attempting to drain response bodies for HTTP/2 connections, when doing so is unnecessary.

The current state and conclusion

Starting in Go 1.27, net/http.Transport automatically does everything we have implemented in the code above: it will asynchronously drain an unread HTTP/1 response body for up to 50 ms or 256 KB. It also implements additional optimizations; for example, when a response contains the Connection: close header, draining the body is guaranteed to be wasted work, and it therefore skips doing so. Note that this behavior might evolve in future versions of Go, as other possible enhancements become apparent.

As a general best practice, we now recommend that users simply close their HTTP bodies once they no longer need them, like in our original example:

func getStatus(url string) (int, error) {
    resp, err := http.Get(url)
    if err != nil {
        return 0, err
    }
    defer resp.Body.Close()
    return resp.StatusCode, nil
}

Go is all about simplicity. Let your code stay clean and simple, and let net/http deal with the complexities on your behalf.

Previous article: Arch-specific SIMD in Go
Blog Index

Read the whole story
alvinashcraft
just a second ago
reply
Pennsylvania, USA
Share this story
Delete

ByteAether.Ulid v1.4.2 Expands the .NET Ecosystem with ASP.NET Core and CLI Support

1 Share

A production-grade identifier library requires tight integration across data persistence layers, API boundary definitions, and operational tooling. ByteAether.Ulid implements a high-performance, specification-compliant .NET type for the ULID standard, engineering strict monotonicity, throughput optimization, and deterministic ordering under high-concurrency workloads.

The ByteAether.Ulid v1.4.2 release extends the ecosystem beyond the core primitive. Alongside existing data-access integrations, the repository (available on GitHub) now provides two specialized packages targeting modern .NET architectures:

  • ByteAether.Ulid.AspNetCore: Native route constraints, System.Text.Json converters, and OpenAPI schema generators.

  • ByteAether.Ulid.Cli: A cross-platform global tool for generating, parsing, and inspecting ULIDs within shell scripts and operational pipelines.

These abstractions eliminate the need to maintain bespoke serialization layers or conversion adapters on top of the primitive Ulid struct.

Architectural Overview: Moving Beyond the Core Type

The ByteAether.Ulid v1.4.0 release established native ORM and micro-ORM integrations across EF Core, LinqToDB, and Dapper. While this solved low-level database mapping challenges, application boundaries remained unoptimized.

Modern enterprise architectures expose two distinct integration boundaries:

  1. HTTP/RPC API Edge: REST endpoints, Minimal APIs, and MVC controllers demand fast route constraint evaluation, efficient model binding, zero-allocation JSON parsing, and schema generation for OpenAPI definitions.

  2. Operational Tooling: DevOps pipelines, database migration scripts, and debugging sessions require CLI utilities to parse timestamps, extract randomness components, and convert byte representations without writing scratch C# applications.

ByteAether.Ulid.AspNetCore and ByteAether.Ulid.Cli address these requirements while preserving the foundational guarantees of the core ByteAether.Ulid repository: zero-allocation parsing, high throughput, and monotonicity under bursty thread scheduling.

Source-First ASP.NET Core Integration

ByteAether.Ulid.AspNetCore Library Logo

ByteAether.Ulid.AspNetCore introduces native framework bindings for ASP.NET Core pipelines:

  • Route Constraints (:ulid): Evaluates incoming 26-character Base32 route parameters at the routing layer before triggering controller instantiation or delegate execution.

  • System.Text.Json Serialization: Direct JsonConverter<Ulid> and JsonConverter<Ulid?> implementations handling canonical string representations without intermediate allocations.

  • OpenAPI Integration: Schema transformers injecting the string format with a ulid designator into both Microsoft.AspNetCore.OpenApi and Swashbuckle metadata models.

  • Native AOT & Trim Compatibility: Fully compatible with source-generated JSON serializer contexts (JsonSerializerContext) required for Native AOT deployments.

The following implementation demonstrates Minimal API endpoint integration with custom route validation:

using ByteAether.Ulid; using ByteAether.Ulid.AspNetCore; var builder = WebApplication.CreateBuilder(args); builder.Services.AddUlidRouteConstraint(); var app = builder.Build(); app.MapGet("/users/{id:ulid}", (Ulid id) => Results.Ok(new UserResponse(id))); app.Run(); public record UserResponse(Ulid Id);

By handling parsing and model binding at the routing boundary, the framework prevents malformed identifier requests from executing pipeline middleware or reaching application handlers.

For API spec generation, registering the schema transformer ensures client code generators emit strongly typed identifier fields instead of generic string definitions.

Operational Tooling: CLI Generation and Inspection

ByteAether.Ulid.AspNetCore Library Logo

ByteAether.Ulid.Cli provides shell-level operations for inspecting and generating ULIDs.

Install the utility via the .NET SDK tool registry:

dotnet tool install -g ByteAether.Ulid.Cli

Command Usage and Scripting Capabilities

Generate a single string identifier:

ulid

Generate sequential identifiers in batches:

ulid --count 5

Parse and inspect internal structure (extracting Unix timestamp and high-precision randomness payload):

ulid 01AN4Z07BY79KA1307SR9X4MV3

Process standard input streams for log analysis or data transformation pipelines:

cat ulids.txt | ulid --json

Utility Features

  • Format Interoperability: Converts canonical Base32 strings into raw Hexadecimal or System.Guid byte layouts.

  • Deterministic Timestamp Overrides: Accepts custom unix epoch timestamps to reproduce state or generate test fixtures.

  • Structured JSON Output: Emits machine-readable breakdown payloads for ingestion by tools like jq or powershell automation.

System Impact and Adoption

The expanded ByteAether.Ulid ecosystem establishes a uniform model for identifier handling across the runtime lifecycle:

  1. Performance Overhead: The core library remains a memory-efficient, zero-allocation struct implementation.

  2. Persistence Integrity: Dedicated ORM providers maintain raw byte-ordering efficiency at the database index layer.

  3. API Edge Precision: ASP.NET Core extensions validate and serialize inputs without custom application layer code.

  4. Developer Experience: Shell utilities standardise operational introspection and test vector generation.

Package Installation

Add the API dependencies or install the global shell utility:

dotnet add package ByteAether.Ulid.AspNetCore dotnet tool install -g ByteAether.Ulid.Cli

Reference material, core source files, and benchmark suites are accessible in the official ByteAether.Ulid GitHub repository. Nuget packages are hosted on the public package index:

Read the whole story
alvinashcraft
12 seconds ago
reply
Pennsylvania, USA
Share this story
Delete

AI Engineering Skills Roadmap

1 Share

Let’s map out a route for learning the AI stack. Follow these four phases, building on your existing developer skills, to map your learning plan.

Over this AI Engineering Basics series so far, we’ve covered the layers of the modern AI stack, the architecture patterns behind AI features, the components that surround a prompt, and the agent layer now growing between frontend and backend.

Knowing the concepts is one thing. Figuring out what to learn first, and how to tell when we’ve learned it, is the part this article takes on. (This post gives you an overall map of what you need to learn and how it builds on your skills as a developer; it does not provide the in-depth training itself.)

AI engineering is an integration discipline, so most of it builds directly on skills full-stack developers already have. We’ll move through four stages, and each one names the competencies to learn and the tools worth reaching for.

How the Roadmap Is Organized

There are four stages, and each one ends with something concrete to build.

StageCore competenciesWhat you can build after it
1. Working with ModelsModel APIs, prompting, structured outputs, streamingA summarization or classification feature
2. Grounding Models in Your DataEmbeddings, vector stores, RAGA docs assistant that cites its sources
3. Actions and AgentsTool definitions, the agent loop, permissions, MCPAn assistant with one scoped write action
4. Running AI in ProductionEvaluation, observability, cost, governanceAn AI feature you can defend shipping

The order is about dependency rather than a strict curriculum. Retrieval builds on model calls, actions build on both, and production work wraps around everything.

Stage 1: Working with Models

What to Learn

This foundation is calling a hosted model from a backend we own and shaping what comes back. That covers the provider APIs themselves and the craft of writing and iterating on instructions. It also covers structured output for when downstream code needs to parse the response, and streaming so users aren’t staring at a spinner.

A little understanding of the underlying theory pays off here as well. An LLM generates text by predicting the next token from everything that came before, and that one fact explains most of what we see day to day: why outputs vary between runs and why a model can state a wrong answer with complete confidence.

Alongside that, this stage builds a working feel for tokens, context windows and cost. Every feature in the later stages runs into all three.

Further Learning

  • OpenAI’s Prompt engineering guide shows how to write clear instructions, provide useful examples and add the context a model needs to produce more consistent results.
  • Vercel’s Builders Guide to the AI SDK uses practical exercises to show how to call models from JavaScript, return structured data and stream responses into a user interface.

Tools to Reach For

Any major provider works: OpenAI, Anthropic or Google. GitHub Models is worth knowing early because it offers a free playground and API access to a wide range of models with just a GitHub account.

What to Build

A summarization or classification feature behind your own backend endpoint, like one that takes a support ticket thread and returns a summary with a suggested priority. It’s small on purpose and proves the request flow and the instruction writing, and it makes the cost math real without any other moving parts.

Stage 2: Grounding Models in Your Data

What to Learn

A model’s built-in knowledge is fixed at training time, so the second stage is retrieval: getting our own content in front of the model at request time. The pieces are embeddings (text represented as vectors that capture meaning), a vector store to search them, chunking strategies for splitting documents and the RAG pattern that ties them together.

Retrieval has a user-facing side too. Grounded answers should cite their sources and admit when the content has no answer.

Further Learning

  • The Progress Telerik AI Crash Course on RAG explains how documents are divided into chunks, converted into embeddings and searched by meaning.
  • OpenAI’s Citation Formatting guide explains how to give each retrieved passage a stable source identifier and instruct a model to cite it.

Tools to Reach For

pgvector adds vector search to a Postgres database we already run, and managed stores like Pinecone take over the operational side. For teams that would rather not build the pipeline at all, governed retrieval platforms like Progress Agentic RAG handle it from ingestion and permissions through to citations.

What to Build

A documentation assistant that answers only from your own content and cites the passages it used. Building it means deciding what the model should retrieve for a given request and figuring out how to keep it from answering beyond the sources. A good sign it’s working: the assistant says the docs don’t cover something instead of guessing.

Stage 3: Actions and Agents

What to Learn

The third stage moves models from answering to acting. The core skill is defining tools: operations with names and typed parameters, plus descriptions that carry the boundaries. The description is what the model reads when deciding whether an action applies. Tool calling runs as a loop: the model requests a call, our code validates and runs it, and the result feeds the next decision.

Agents also need limits. Scoped permissions let them read broadly and write narrowly, and human approval checkpoints sit in front of anything hard to reverse. The Model Context Protocol (MCP) belongs here too, as the standard for how agents discover and call tools.

Further Learning

  • OpenAI’s Function calling guide shows how to define a tool with typed parameters, receive a tool request from the model, execute the operation in application code and return the result to the model.
  • The official Build an MCP server tutorial walks through creating a server that exposes callable tools and connecting it to an MCP host. Examples are available for TypeScript, Python and several other languages.

Tools to Reach For

Every major provider supports tool calling through the same API we learned in Stage 1 (Working with Models), so no new platform is required to start. The MCP documentation and SDKs are the place to learn the protocol, and orchestration frameworks like Progress Forge help once workflows grow past a single loop.

What to Build

An assistant with exactly one write action behind a confirmation step, like a project assistant that can move tasks to the next sprint after we approve the list. One action is all it takes to run into every hard part: the definition, the permission scope, the approval UI and the log of what ran. When we can pull up that log and explain what the agent did and why, this stage is done.

Stage 4: Running AI in Production

What to Learn

Everything up to this point can be demoed in a week. This stage is about what happens after real users show up. Model outputs change from run to run, so testing becomes evaluation: building a set of graded examples and scoring the system against them after every change.

Observability follows. Each request gets traced from retrieval and tool calls through to the final answer, so failures can be reconstructed.

The remaining competencies are cost monitoring and the security and governance basics. Token spend scales with usage in ways traditional features don’t, and the security questions come down to what data reaches the model and who is allowed to trigger which actions.

Further Learning

The Progress Telerik How to Trace and Debug AI Agents in Production webinar shows how tracing helps us understand what an AI agent actually did when something goes wrong. It also covers evaluating answers, catching prompt injection and finding the calls responsible for slow responses or unexpected costs.

Tools to Reach For

Progress Agent Engineering, an AI observability platform, shows where workflows break, what they cost and whether outputs are good enough to ship. It is built for teams working in .NET, Python and JavaScript.

What to Build

Not a new feature this time. Wrap the assistant from the earlier stages in an eval suite of 20 or 30 graded examples and full request tracing, then change the prompt and watch the scores move. The goal is a workflow where a change ships with score evidence attached rather than a hunch.

Skills That Cut Across Every Stage

Some competencies don’t belong to any single stage. Four of them show up everywhere.

AI-Native UX Judgment

This is knowing when to show a confidence indicator, where a human-in-the-loop moment belongs, when chat is the wrong interface for the job and how to present sources so users can check an answer. A model call that works can still ship as a bad feature, and the difference is usually this judgment rather than the engineering.

Component libraries have started shipping these patterns ready-made, like the Progress KendoReact Conversational UI components, which leaves the judgment calls to us instead of the widget-building.

A Security Instinct

Every input a model reads is a potential instruction, which is what makes prompt injection a new class of attack. Every piece of context we assemble is also data leaving our boundary. Deciding what may reach the model and what its output is allowed to trigger is a per-feature decision, and it starts in Stage 1 (Working with Models) rather than arriving with Stage 4 (Running AI in Production).

Designing for Non-Determinism

The same request can produce different outputs on different runs, and ever-stricter prompts won’t make that go away. Non-determinism is a design constraint rather than a bug. Designing for it means constraining the output format and validating everything the model returns. It also means building a fallback for the times the model returns something unusable.

Working with Coding Agents

Using AI-assisted development tools daily doubles as study for this roadmap. Working with a coding agent in Cursor, Claude Code, OpenAI Codex or GitHub Copilot is a live lesson in how models handle context and instructions, and that intuition transfers directly to the features we build.

The tooling also runs in the other direction. Libraries are starting to ship MCP servers of their own, like the Telerik and Kendo UI MCP Server, which gives a coding agent specialized knowledge of the component library it’s generating code for. That’s the same tool discovery idea from Stage 3 (Actions and Agents), applied to our own workflow.

Wrap-up

This series opened with the claim that the full-stack role is expanding rather than being replaced. The articles since have covered the stack, the architectures, the components beyond the prompt and the agent layer.

This roadmap turns those ideas into things to build. None of the checkpoints need special infrastructure or a research background, and none of them are bigger than a side project.

The series continues from here as well. Upcoming articles will go deeper into the territory each stage opens: designing AI-powered experiences, agent engineering, running AI in production and AI-assisted development itself.

For more on building AI-powered applications and agents with Progress, check out the following resources:

Read the whole story
alvinashcraft
22 seconds ago
reply
Pennsylvania, USA
Share this story
Delete

Visual Studio Code 1.143 (Insiders)

1 Share

Learn what is new in Visual Studio Code Insiders, including the latest features and improvements.

Read the full article

Read the whole story
alvinashcraft
46 seconds ago
reply
Pennsylvania, USA
Share this story
Delete

New Tools, New Workflows and the Value of the Visual Studio Subscription: What We Learned at VSLive! San Diego

1 Share

VSLive SD recap blog banner imageWhat happens when developers who have spent decades working with Visual Studio start rethinking how they write code, learn new skills, and expand their access to development tools? 

At VSLive! San Diego, we had an opportunity to find out as developers gathered for five days of technical sessions, hands-on learning, and conversations about Visual Studio, .NET, AI, cloud, data, and modern application development.  

One of the most interesting things we heard in San Diego was how readily experienced developers are adopting new tools and workflows. 

GitHub Copilot is part of that story, and developers aren’t limiting themselves to a single AI experience. They’re building broader, more differentiated tool kits. But they aren’t necessarily replacing familiar tools. They’re expanding how they work, choosing different experiences depending on the task, and finding new ways to solve problems.  

This is why events like VSLive! provide such an amazing opportunity for developers tackling these kinds of changes. Not only do they get to learn from expert led sessions and hands on labs, they get to learn from each other. 

AI is changing how developers approach unfamiliar problems 

One of the best parts of this event was being able to explore how developers learn new technologies and work through challenges.  

AI is increasingly becoming an early step in that process. 

Developers are using AI to research unfamiliar topics, understand existing code, troubleshoot problems, and explore possible approaches before deciding what to try next. 

But they aren’t relying on AI alone. 

They still move between documentation, technical videos, sample code, and hands-on experimentation to develop a deeper understanding of what they’re building. And a significant number of participants shared that hands-on learning was a preferred way to learn. 

That stood out to me. 

Even as developers adopt AI-assisted workflows, they continue to value opportunities to work directly with technology, test ideas, and understand how things behave. AI may help developers get started faster. Experimentation helps them decide what to do with what they’ve learned. 

The suite of benefits offered through a Visual Studio Subscription complements this developer journey, not only by providing significant discounts at VSLive! events, but also through access to learning paths, Azure credits, certifications, and hands-on labs.  

More generated code means more to evaluate 

Another interesting observation was how AI is changing the work that happens after code is written. Developers told us AI helps them produce code faster, but that doesn’t necessarily mean they’re spending less time on development. 

Instead, they’re spending more time reviewing and validating generated code. Does it work as expected? Does it fit the architecture? Is it secure? Can someone maintain it later? 

These questions have always mattered. AI-assisted development makes them even more important as developers evaluate code they may not have written themselves. 

AI can accelerate development, but developer resourcing and judgment remains essential. 

Time is still the biggest obstacle to learning 

With so many learning resources available, you might expect developers to struggle with finding the right information. But our conversations with developers at VSLive! San Diego signaled that the biggest barriers to learning new skills are time and the pace of technological change. 

Sometimes developers need a quick explanation to move forward. Other times, they need a course, a technical session, a community, or an environment where they can experiment. 

That helps explain why the flexible learning environment at VSlive! matters now more than ever. And it’s another reason Visual Studio Subscription benefits remain valuable. They aren’t just about the software developers use today. They can also help support the technologies and skills developers want to explore next. 

Let’s keep the conversation going 

What stood out most from San Diego was the combination of experience and curiosity. We met developers who have used Visual Studio for decades and are actively exploring how AI can change the way they build software. 

They’re adopting new tools, reconsidering familiar workflows, and looking for more opportunities to experiment. And they’re helping us better understand what developers need as those workflows continue to evolve. 

That’s why the conversations we have at events like VSLive! matter. They give us a chance to hear directly from the people using our tools and help us better understand where we can improve. 

If you’d like to continue learning and connecting with the developer community, join us at future VSLive! events: 

  • Orlando November 15–20, 2026  

Thanks to everyone who joined us in San Diego, and especially those who shared their experiences and ideas. The tools developers use are evolving quickly. Hearing how developers are adapting helps us understand what we should build and improve next. 

The post New Tools, New Workflows and the Value of the Visual Studio Subscription: What We Learned at VSLive! San Diego appeared first on Visual Studio Blog.

Read the whole story
alvinashcraft
51 seconds ago
reply
Pennsylvania, USA
Share this story
Delete

Create Reusable, Schema-Driven Forms with Syncfusion Form Builder and Form Renderer

1 Share

Create Reusable, Schema-Driven Forms with Syncfusion Form Builder and Form Renderer

TL;DR: Build schema-driven forms in Angular with Syncfusion Form Builder and Form Renderer. Design forms visually, export their configuration as a JSON schema, and render interactive forms with configured fields and validation rules. This approach separates the form definition from rendering logic and simplifies form maintenance.

Have you ever needed to update the same form across multiple applications whenever a field, label, or validation rule changes? Maintaining separate form definitions can lead to duplicated code and inconsistent behavior.

What if you could define a form once and reuse its schema wherever needed? Syncfusion® Form Builder and Form Renderer provide a schema-first approach to form development. Form Builder lets you design forms visually and export a JSON-based schema, while Form Renderer uses that schema to generate an interactive form with the defined fields, layout, and validation rules.

In this blog, you’ll explore how these components work together, understand the form schema, and learn how to render a form and handle its submission.

What Is Syncfusion Form Builder?

Syncfusion Form Builder is a visual form designer that lets you create forms by dragging and dropping fields onto a design canvas. Instead of manually defining every field in code, you can configure the form through the designer and inspect the generated JSON schema.

The Form Builder interface has five main areas:

  • Toolbox: Displays the available form fields you can add to the form.
  • Design canvas: Provides the workspace for adding and arranging fields.
  • Properties pane: Lets you configure the form and selected fields.
  • Code view: Displays the generated form schema as you make changes.
  • Form preview: Shows how the form will appear and behave.

You can drag a field from the toolbox onto the design canvas or use the canvas’s add button to insert one through the context menu. After adding a field, configure its properties, such as its label and validation rules, through the properties pane.

As you make changes, Form Builder updates the schema to reflect the form configuration. You can inspect the schema in the code view, preview the form, and export the definition for use with Form Renderer.

For setup instructions, refer to the Angular Form Builder getting-started guide.

How does the schema-first approach work?

A form schema describes the structure and configuration of a form, including its fields, layout, validation rules, and settings. Form Builder creates this definition, and Form Renderer uses it to generate the form at runtime.

The process consists of four steps:

  1. Design: Add fields and configure their properties in Form Builder.
  2. Export: Generate the JSON schema containing the form definition.
  3. Render: Pass the schema to Form Renderer through its schema property.
  4. Handle submission: Use the renderer’s submit event to access the submitted data and validation status.

This separation keeps the form definition independent of the application code that displays and processes it. When a form needs to change, you can update the schema and ensure that the relevant applications load the updated version.

Form Builder interface
Form Builder interface

What is Syncfusion Form Renderer?

Syncfusion Form Renderer is a schema-driven component that converts a structured form definition into an interactive form at runtime. It uses the supplied schema to determine which fields to display, how to arrange them, and which validation rules to apply.

Let’s look at an Angular example that defines a login form with an email field, a password field, a Remember Me checkbox, and a submit button.

Define the Form Schema in Angular

The following example defines the form schema and passes it to the Form Renderer component.

import { Component, ViewEncapsulation } from '@angular/core';
import { FormRendererModule, Schema } from '@syncfusion/ej2-angular-form-renderer';

@Component({
    selector: 'app-root',
    template: `<div>
                  <div
                    id="form-renderer-control"
                    ejs-form-renderer
                    [schema]="formSchema"
                    (submit)="onSubmit($event)">
                  </div>
               </div>`,
    styleUrls: ['app.component.css'],
    encapsulation: ViewEncapsulation.None,
    standalone: true,
    imports: [FormRendererModule]
})
export class AppComponent {
    formSchema: Schema = {
        "version": "0.1.0",
        "properties": {
            "emailAddress": {
                "id": "textbox_1785491685456_167",
                "name": "emailAddress",
                "type": "string",
                "label": "Email Address",
                "textboxType": "email",
                "required": true,
                "widget": "textbox"
            },
            "password": {
                "id": "textbox_1785491685456_537",
                "name": "password",
                "type": "string",
                "label": "Password",
                "textboxType": "password",
                "required": true,
                "minLength": 6,
                "widget": "textbox"
            },
            "rememberMe": {
                "id": "checkbox_1785491685456_262",
                "name": "rememberMe",
                "type": "boolean",
                "label": "Remember Me",
                "widget": "checkbox"
            },
            "submit": {
                "id": "submit_button_initial",
                "name": "defaultFormsubmit",
                "type": "button",
                "label": "Submit",
                "buttonType": "submit",
                "widget": "button",
                "style": "primary",
                "disabled": false
            }
        },
        "layout": [
            { "type": "field", "propertyId": "emailAddress" },
            { "type": "field", "propertyId": "password" },
            { "type": "field", "propertyId": "rememberMe" },
            { "type": "field", "propertyId": "submit" }
        ],
        "settings": {
            "name": "Untitled Form"
        }
    };

    public onSubmit(args: any): void {
        console.log('Is Valid:', args.isValid);
        console.log('Submitted Data:', args.data);

        if (args.isValid) {
            console.log('Processing form submission...');
        }
    }
}

The rendered result appears as follows.

Form Renderer output
Form Renderer output

Form Renderer uses the schema property to build the form and the submit event to invoke the onSubmit() method.

The schema includes three input fields and a submit button. It marks the email and password fields as required and applies a minimum length of six characters to the password. This rule illustrates schema-based validation and should not be treated as a production password policy.

Understand the Form Schema

The schema organizes the form configuration into three main sections:

  • properties: Defines the form fields, including their names, labels, data types, widgets, and validation rules.
  • layout: Specifies the order in which fields appear. Each entry references a field through its propertyId.
  • settings: Stores form-level configuration, such as the form name.

The widget property determines the type of control used to render a field. This example uses textbox, checkbox, and button widgets.

Validation rules are defined alongside their corresponding fields. The email and password fields use required, while the password field also uses minLength. Other supported schema rules include maxLength and pattern, depending on the field configuration.

Note: Client-side validation helps identify input errors, but it does not replace server-side validation or authorization. Always validate submitted data on the server before processing it or allowing access to protected resources.

For setup details and additional schema examples, refer to the Angular Form Renderer getting-started guide.

How form submission works

Once the form is rendered, users can enter their details and select the Submit button. Because the button uses "buttonType": "submit", Form Renderer validates the form and raises its submit event.

The event provides the submitted form data through args.data and the validation status through args.isValid.

In the Angular example, the onSubmit() method logs both values:

public onSubmit(args: any): void {
    console.log('Is Valid:', args.isValid);
    console.log('Submitted Data:', args.data);

    if (args.isValid) {
        console.log('Processing form submission...');
    }
}

The submitted data is keyed by the field names defined in the schema. For this example, it can contain values associated with:

  • emailAddress
  • password
  • rememberMe

The submit button is not included as an input value in the submitted data object.

Checking args.isValid lets the application determine whether the form passed validation before proceeding. In a real application, that step might involve sending the validated data to a server for further validation and processing.

You can also explore the Form Renderer StackBlitz sample to see the control in action.

Form Builder vs. Form Renderer

Form Builder and Form Renderer work together, but each has a distinct responsibility.

Form Builder Form Renderer
Provides a visual form-design interface Generates an interactive form from a schema
Lets you add, arrange, and configure fields Maps field definitions to supported widgets
Lets you configure field properties and validation rules Applies the configured layout and validation rules
Provides a preview and schema export Handles form interactions and raises form-action events
Produces the form schema Provides submitted form data to the application

This separation means that an application does not need to recreate the form structure manually. It can use a supported schema and pass it to Form Renderer.

Why shared schemas simplify form development

A schema-first approach is useful when form definitions need to be reused or updated regularly. Here are three practical benefits.

Avoid maintaining separate form definitions

When multiple applications use the same form, maintaining separate markup and validation logic makes updates repetitive. A shared schema brings field definitions, layout, and validation rules together, reducing duplicated form configuration.

Keep validation rules close to field definitions

Rules such as required and minLength can be defined alongside their corresponding fields. When validation requirements change, you can update the schema instead of modifying separate validation logic. Applications must load the updated schema for the changes to take effect.

Simplify form updates across applications

Consider a customer onboarding form used in a self-service portal, an administrator dashboard, and an internal support application. Instead of updating three separate form definitions, you can maintain one shared schema for applications that support it. Each application must load the appropriate schema version to reflect the changes.

Where can these components be useful?

Form Builder and Form Renderer are useful for applications that need configurable or frequently updated forms, including:

  • Customer onboarding forms
  • Surveys and feedback forms
  • Support and service-request forms
  • Internal approval forms
  • Configurable intake forms
  • SaaS applications that allow users to design forms
  • Low-code and no-code development platforms

For example, a service team may need to add a field to a support-request form when its process changes. A shared schema can reduce the work involved in maintaining that form across multiple applications, provided each application loads the updated definition.

The approach is particularly useful when form definitions change more frequently than the application code responsible for displaying them.

Getting started with Form Builder and Form Renderer

Ready to try schema-driven forms in your app? Use the following resources to explore the components and their configuration options.

Resource Link
Angular Form Builder Getting Started
Angular Form Renderer Getting Started
Form Renderer schema reference Form Schema
Form Renderer examples Angular demos

Working with another framework? Check the corresponding Syncfusion documentation to ensure both packages fit your application setup.

Harness the power of Syncfusion’s feature-rich and powerful Angular UI components.

Start building schema-driven Forms

Maintaining forms across applications does not have to mean maintaining separate definitions for every instance. Syncfusion Form Builder and Form Renderer separate form design from rendering, allowing you to create a form visually, export its JSON schema, and generate interactive forms with preconfigured fields and validation rules.

By keeping the form definition in one reusable schema and ensuring that applications load the appropriate version, you can reduce duplicated definitions and simplify future updates.

Design the form, reuse its schema, and make form maintenance easier.

Explore the complete Essential Studio Volume 3 What’s New and Release Notes documentation to find the features relevant to your project.

Existing Syncfusion customers can download the latest version from the license and downloads page, while new users can get started with a free 30-day trial.

Have questions or feedback? Connect with us through the Syncfusion support and feedback channels.

Read the whole story
alvinashcraft
1 minute ago
reply
Pennsylvania, USA
Share this story
Delete
Next Page of Stories