Oct 10th 2025

Turn AI Prompts into Web Apps using a Semiformal DSL

Soulaymen ChouriSoulaymen Chouri
Dr. Steven SmythDr. Steven Smyth

Coding with LLMs often feels like casting spells—magical when it works, frustratingly vague when it doesn’t. What we call vibe coding—prompting an AI until it “feels right”—is powerful but unpredictable. In our previous blogpost , we explored how ambiguity, verbosity, and vagueness limit prompt reliability. Minor wording changes can lead to wildly different results, making it hard to reproduce or maintain behavior over time. At the same time, the advantages of using LLMs are undeniable. They enable new modes of interaction between humans and machines—agents—and even between agents themselves. To tame the unpredictability of vibe coding, we’ve been exploring a new approach: a semiformal DSL that gives structure to prompts without limiting flexibility.

A recent trend in LLM workflows is specification-driven development, where either the user or the model outlines a step-by-step plan before execution. Semiformal DSLs take a different path: instead of planning first, they embed structure within the interaction itself. They let you be precise where needed, and flexible where creativity is welcome. Instead of struggling with prompt engineering or repeatedly translating your domain into something the LLM might understand, you can use a language tailored for this kind of interaction—understandable by both humans and LLMs, and grounded in the domain you care about. Compared to traditional DSLs, semiformal ones even offer an even richer, more expressive language. This gives domain experts the freedom to speak in their own terms.

In this blog, we present SWAG—a semiformal DSL tailored for prompting LLMs to create web applications. When it comes to creating web applications via LLMs, there exists multiple solutions, such as loveable, v0.dev, bolt.new, and kiro.dev. All of these websites feature an agent-driven development based on natural text often first formulating a plan on how to proceed with the task. In contrast, SWAG acts as a true specification, describing the exact state (including all constraints) of the application, rather than just enumerating the steps that need to be achieved.

The typical development of web applications splits the backend and the frontend into two separate projects connected by HTTP requests. This pattern may vary—for example if the backend uses Firebase—but the principle remains the same. A key requirement is that the APIs exposed by the backend are properly consumed by the frontend applications. Creating a web application with an agent involves progressing through several phases to build the frontend and backend. For this process to succeed, the interfaces between phases must be consistent and unambiguous—otherwise, the resulting app will simply break. This is where the structured part of the DSL becomes essential.

The SWAG Grammar

In SWAG, there are three core constructs: entity, component, and action.

Entities define the precise data model, which must remain consistent across frontend and backend—there’s no room for interpretation here.

entity Project {
  id: number
  name: String
  description: String
  createdAt: datetime
  updatedAt: datetime
  files: File[]
}

Components represent user interface elements.

component Toolbar {
  runButton: ui::button
  saveButton: ui::button
  undoButton: ui::button
  redoButton: ui::button
  createFileButton: ui::button
}

Actions describe the interactions initiated by the frontend and handled by the backend.

action loadProject {
  route: "/load-project"
  method: "GET"
  params: "projectId"
  returns: "select * from Project where id = projectId"
}

Certain aspects, such as the layout or visual representation of the toolbar, are intentionally left unspecified. This gives the LLM room to decide what makes the most cohesive UI, without burdening the developer with design or implementation details.

Still, when needed, developers can dial up the precision by adding more definitions or annotating with natural-language hints. The key idea is: the level of formality is up to the developer. Importantly, iterations can (and should) happen at the level of the DSL—not in the generated code. The LLMs output is not just code, but a continuation of the conversation, written in the same semiformal DSL. Since we designed SWAG with Langium, we get tooling out of the box to experiment with and improve the grammar.

Engineering a DSL for LLM Prompting

Designing a semiformal DSL is about giving experts structured yet flexible language that bridges their domain expertise with the LLM’s generative capabilities. For SWAG, we combined familiar constructs with the right balance of formality and creative freedom—so both humans and AI agents can work together effectively.

SWAG User/LLM interactions

Why This Matters

Classic natural-language prompts such as:

Create a web app with a user id being a string.

can result in inconsistencies like the LLM generating userID in the backend and user_id in the frontend. By contrast, formal DSL elements like:

entity User {
    id: String
}

anchor the LLM, ensuring it generates consistent and compatible code across the stack.

At the same time, semiformal flexibility allows domain experts to progressively tighten requirements—starting vaguely at first during exploration, then adding precision as the design matures. The design of a semiformal DSL like SWAG combines the best of formal and informal worlds, creating a shared, reliable language. Together they turn vibe coding from guesswork into an iterative, structured conversation.

Designing for a Specific Domain

Designing a (semiformal) DSL for prompting LLMs is usually more beneficial when you are performing repetitive prompts.

Our goal is to generate a web application that is guaranteed to build and run without (or with very minimal) user intervention. For this, we set out to try different technologies and settled on React for the frontend. This decision was rather straightforward, as LLMs output quality directly correlates to their training data, which explains why it would fail on using frameworks that are less documented or hardly used. While this limitation can be mitigated, e.g., by additional documentation or examples based on the desired tech stack, that’s an area for future exploration.

Design Choices and Specifications for SWAG

The main design choices for SWAG are guided by the following principle: Make LLM prompting reliable without sacrificing expressiveness.

  1. Natural yet unambiguous syntax SWAG’s grammar borrows from familiar syntaxes like TypeScript, JSON, and other developer-friendly DSLs. For example, entities, components, and actions are defined in straightforward blocks. This reduces surprises for both the developer and the LLM, avoiding exotic or overly terse syntax.

    entity Project {
        id: number
        name: String
    }
    
    component Toolbar {
        saveButton: ui::button
    }
    
    action loadProject {
        route: "/load-project"
        method: "GET"
    }
    
  2. Composable design Attributes within components or actions can reference other entities, components, or built-in UI types. This supports deep, flexible modeling of applications.

  3. Direct mapping to MVC-like concepts Our grammar directly reflects an MVC-like architecture:

    • Entities for data models
    • Components for reusable UI pieces (View)
    • Controllers for backend endpoints
    • Pages for root UI containers

    This alignment allows the LLM to reason about the architecture at a high level without additional explanation.

  4. Semantically meaningful keywords Keywords need to be inferred from the domain. Since our DSL is targeted for generating web apps, they are chosen to match general web vocabulary (component, action, etc.).

  5. Focus on small DSL size We deliberately designed SWAG to keep its syntax small enough to fit in system prompts (few-shot examples) but expressive enough to cover key project specifications. While this is not a firm boundary, you should keep LLM’s context size in mind. The DSL and its semantics should fit without any bloat.

  6. Support for both abstract and concrete UI hints Our grammar allows developers to add natural-language comments as hints to guide design choices, such as describing a component’s behavior or layout. For example:

    component CodeEditor {
        editorPane: ui::textarea
        // This would act like a Monaco code editor instance
    }
    
  7. AI-Friendly DSL SWAG’s DSL is designed for both input (prompts from the user) and output (responses from the LLM). For example, it is possible to generate SWAG code via Coding Agents such as Cursor or Copilot via simple rules/system prompts. This of course depends on the complexity of the DSL and prompt quality.

What Should Be Formal vs. Semiformal?

A key insight in designing semiformal DSLs is that not all information benefits from being formalized—nor should everything be left informal. The challenge lies in striking the right balance.

Formal Parts

These must be strictly defined and consistent. It is possible to extend them with comments as a form of informal description, but they have mandatory formal properties.

  • Entities Data models must be compatible across the database, backend, and frontend. For example, if we model our entities as follows:

    entity User {
        // Unique ID
        id,
        // At least 8 characters long and not `password` or `12345678`
        password
    }
    

    While we informally explained that the ID is unique, the actual structure is ambiguous. The LLM might interpret id as a Serial in the backend and a String in the frontend. To avoid this, we formally model the entities with their data types.

    entity User {
        id: String,
        password: String
    }
    
  • Actions: routes and methods Backend contracts require exactness—any variation can break frontend-backend protocol and communication.

Semiformal Parts

These can mix formal structure with free-form hints, often serving as informal points for creativity, suggestions, or abstract requirements:

  • Components While properties like textArea: ui::textarea are formal, developers can add natural-language comments or leave aspects (layout, styling) vague for the LLM to infer.
  • Pages Define high-level intent but let the LLM decide on layout details or composition strategies.
  • Layout hints E.g., “Show key metrics at the top” or “Use a sidebar for navigation.”
  • Tech stack suggestions The optional stack = […] at the top of the model lets developers express preferences like [“react”, “express”], giving the agent direction without being overly prescriptive.

Workflow vs Agent Design

SWAG’s code generation pipeline is designed as a workflow and not as an agent. The generation process is deterministic: We first prompt the LLM to generate the backend code, then we generate the frontend code.

The motivation for this choice is both simplicity and predictability. There is no need for the agent to use tools or explore context outside of what it already has in the DSL file. Furthermore, parts of the pipeline can be re-run independently, enabling incremental designs.

Minimizing room for errors

As previously mentioned, the web application and the backend need to define and adhere to a specific communication API. Since this API is defined by the backend, the backend code is generated first. In our example, we use postgres as a backend database, and either koa or express for the server. Both yielded consistent and reliable results in our tests.

After generating the backend, the LLM also outputs a notes section, documenting the generated API, routes and data model. These notes serve as context for generating the frontend, so the model knows exactly which API routes to consume and what their inputs and outputs are. This is critical as we do not want the LLM to speculate about these routes.

For example: Even if the routes are explicitly modeled in the DSL, the LLM might still add a /api prefix or invent additional endpoints. Also, in some cases, routes might not be defined in the DSL at all.

Such challenges typically depend on the DSL and how it is modeled. In SWAG, we’ve opted for more flexibility, which means we had to account for potential gaps to prevent the LLM from speculating.

Using the Semiformal DSL

With the DSL being formally described, and code being generated using the DSL as a source reference, we suggest thinking of the code as an end result. Meaning, any changes to the project specifications would ideally be modeled in the DSL, instead of in the generated code. This is important as it guarantees several properties:

  • Reproducibility: Being able to achieve the same (or very similar) results.
  • Consistency Checks: Helps you identify areas where the DSL structure itself could benefit from more improvements, such as augmenting it with new constructs to model new criteria or behaviors, in cases where the output is not consistent.
  • Version Control Clarity: DSL changes are semantic and meaningful in git history.
  • Refactoring Safety: Major architectural changes (like switching from REST to GraphQL) become DSL modifications rather than manual rewrites.
  • Specification-Driven The DSL serves as living documentation that’s always in sync with the actual system, unlike traditional docs that drift from reality.

Even when certain changes cannot yet be expressed in the DSL, it is still useful to include the DSL file alongside your source code. This allows AI assistants and agents, such as Cursor, to use it as a contextual reference for the project code base and structure.

It’s also possible to apply formal tooling over the DSL. For example:

  1. Architecture Diagrams: Parse the DSL to automatically generate entity-relationship diagrams, component hierarchies, and action flow charts.
  2. API Documentation: Extract all actions from the AST to generate OpenAPI/Swagger specs, Postman collections, or interactive API documentation with proper types and examples.
  3. Data Flow Visualization: Analyze which components consume which entities and through which actions, creating visual maps of how data moves through your application.
  4. Dependency Graphs: Visualize the relationships between entities, components, and actions to identify potential circular dependencies or orphaned elements

Of course, this all depends on how the DSL is modeled and how formal or informal some parts are.

SWAG Example

As a complete example, we will create a simple calculator web application using SWAG. The calculator is solely specified in the DSL and no hand-written code is required.

/** 
 * Represents a calculation entry in the history
 */
entity CalculationHistory {
    expression: String
    result: String
    timestamp: datetime
}

/**
 * Calculator display component showing both current input and result
 */
component Display {
    value: String
    result: String
}

/**
 * Calculator button component
 */
component CalcButton {
    /** Text to display on the button */
    label: String
    /** Optional color style for the button */
    style: String
}

/**
 * History display component showing past calculations
 */
component HistoryList {
    history: CalculationHistory[]
}

/**
 * Main calculator component combining display, buttons, and history
 */
component Calculator {
    display: Display
    /** Grid layout for calculator buttons */
    buttonGrid: {
        numbers: CalcButton[]
        operators: CalcButton[]
        /** Red background button for emphasis */
        equals: CalcButton
        clear: CalcButton
    }
    history: HistoryList
}

/**
 * Action to perform calculation and store in history
 */
action calculate {
    route: "/api/calculate"
    method: "POST"
    returns: "{ result: string }"
}

/**
 * Action to fetch calculation history
 */
action getHistory {
    route: "/api/history"
    method: "GET"
    returns: "CalculationHistory[]"
}

/**
 * Main calculator page
 */
page CalculatorPage {
    calculator: Calculator
} 
SWAG Example

Is there a Problem with Vibe Coding?

It’s fair to ask how our approach—using a DSL for prompting—compares to the growing trend of vibe coding. Let’s break down the differences:

  1. Vibe Coding: Through natural language, the user describes the desired outcome to the LLM or provides it with direct commands or tasks. The AI interprets these prompts, filling in gaps based on context, patterns, and training data. For example:

    Create a modern login form with validation

    This might yield a functional React component—or something else entirely.

  2. Semiformal DSL: The user uses a (semi) structured language, designed specifically for interacting with AI in the context of a domain— in this case generating web applications. This DSL combines formal syntax with natural language elements.

Vibe coding delivers impressive results while being loose and conversational. The user takes on more of a manager or reviewer role prompting the agent to iterate multiple times on the generated code refining it over time. A semiformal DSL, by contrast, acts as a blueprint: a specification that is expected to be implemented but allows for embedding natural language for both context and intent. It does not constrain the AI’s creativity—it channels it through structure and concreteness. In short, we are comparing a task description to a shared specification.

Ultimately, the choice between Vibe Coding and Semiformal DSLs depends on the specific needs and goals of the project. For exploratory prototypes, vibe coding excels. But currently, for enterprise-grade projects, relying solely on agent-based solutions is unacceptable, because the technical debt accumulates too quickly. While vibe coding is powerful, we believe that it needs more structure and formality to truly scale effectively.

In this post, we have explored a use-case of a semiformal DSL—generating web applications with SWAG—but there are plenty more: generating data, documentation, workflows, actions, and more. As language engineers and domain experts, we will keep exploring how semiformal DSLs can deepen our interactions with LLMs—without compromising precision.

About the Authors

Soulaymen Chouri

Soulaymen Chouri

Soulaymen enjoys writing all kind of software. He likes implementing complex algorithms and coming up with new ideas and solutions. Outside of work, he does more work, maintaining a Piano learning application and building small open source projects.

Dr. Steven Smyth

Dr. Steven Smyth

Steven is a seasoned programming languages enthusiast with over 30 years of experience. With a background in real-time systems and a decade of teaching experience, he is passionate about creating pragmatic solutions that empower other developers. Beyond software, he enjoys tinkering with legacy systems, electronics, and 3D printing.