REST API Generator

BESSER offers a code generator for REST API utilizing the FastAPI framework. This tool automatically transforms classes and relationships defined in a Structural model into a RESTful service.

Let’s generate the code for the REST model of our Structural model example structural model example. You should create a RESTAPIGenerator object, provide the Structural model, and use the generate method as follows:

from besser.generators.rest_api import RESTAPIGenerator

rest_api = RESTAPIGenerator(model=library_model, http_methods=["GET", "POST", "PUT", "PATCH", "DELETE"], backend=False)
rest_api.generate()

Parameters

  • model: The input B-UML structural model (required).

  • http_methods: List of HTTP methods to generate (default: ["GET", "POST", "PUT", "PATCH", "DELETE"]).

  • backend: When True, generates the modular FastAPI application layer used by the BackendGenerator instead of the standalone rest_api.py (default: False).

  • port: Port number embedded in the generated FastAPI app’s uvicorn.run call (optional, default 8000).

  • output_dir: Output directory (default: output/ in the current directory).

Note

REST API vs. Backend Generator: The REST API generator produces the API layer only. The Backend generator builds on top of this by also generating the SQLAlchemy ORM models and Pydantic validation models the API imports. Use the REST API generator when you only need API scaffolding; use the Backend generator when you need a complete backend with database.

See also

Need endpoints the model does not describe, or auth on top of them? The Spec-Driven Agent runs this generator (or the Backend generator) first, then lets an LLM customise the result to a natural-language request.

Output Files

With backend=False (the default), the generator creates two files in the output directory:

  • rest_api.py: FastAPI application with CRUD endpoints for each class.

  • pydantic_classes.py: Pydantic validation models generated by the Pydantic generator.

With backend=True, it renders the modular application layer instead (shared with the Backend generator, which is the usual entry point):

  • main_api.py: slim app setup that wires in one router per class (run it with uvicorn main_api:app).

  • routers/<class>.py: one router module per class with all of its endpoints.

  • database.py: shared engine/session setup honoring the DATABASE_URL environment variable.

  • bal_stdlib.py: B-UML Action Language helpers shared by the routers.

  • requirements.txt: dependencies of the generated application.

The sql_alchemy.py and pydantic_classes.py modules that main_api.py imports are not generated in this mode — they remain the caller’s responsibility, as the Backend generator demonstrates.

The generated output for the library example is shown below.

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
import os, json
from fastapi import FastAPI, HTTPException
from pydantic_classes import *

app = FastAPI()

############################################
#
# Lists to store the data (json)
#
############################################

book_list = []
author_list = []
library_list = []


############################################
#
#   Book functions
#
############################################
@app.get("/book/", response_model=List[Book], tags=["book"])
def get_book():
    return book_list

@app.get("/book/{attribute_id}/", response_model=Book, tags=["book"])
def get_book(attribute_id : str):   
    for book in book_list:
        if book.id_to_change== attribute_id:
            return book
    raise HTTPException(status_code=404, detail="Book not found")

@app.post("/book/", response_model=Book, tags=["book"])
def create_book(book: Book):
    book_list.append(book)
    return book

@app.put("/book/{attribute_id}/", response_model=Book, tags=["book"]) 
def change_book(attribute_id : str, updated_book: Book): 
    for index, book in enumerate(book_list): 
        if book.id_to_change == attribute_id:
            book_list[index] = updated_book
            return updated_book
    raise HTTPException(status_code=404, detail="Book not found")

@app.patch("/book/{attribute_id}/{attribute_to_change}", response_model=Book, tags=["book"])
def update_book(attribute_id : str,  attribute_to_change: str, updated_data: str):
    for book in book_list:
        if book.id_to_change == attribute_id:
            if hasattr(book, attribute_to_change):
                setattr(book, attribute_to_change, updated_data)
                return book
            else:
                raise HTTPException(status_code=400, detail=f"Attribute '{attribute_to_change}' does not exist")
    raise HTTPException(status_code=404, detail="Book not found")

@app.delete("/book/{attribute_id}/", tags=["book"])
def delete_book(attribute_id : str):   
    for index, book in enumerate(book_list):
        if book.id_to_change == attribute_id:
            book_list.pop(index)
            return {"message": "Item deleted successfully"}
    raise HTTPException(status_code=404, detail="Book not found") 

############################################
#
#   Author functions
#
############################################
@app.get("/author/", response_model=List[Author], tags=["author"])
def get_author():
    return author_list

@app.get("/author/{attribute_id}/", response_model=Author, tags=["author"])
def get_author(attribute_id : str):   
    for author in author_list:
        if author.id_to_change== attribute_id:
            return author
    raise HTTPException(status_code=404, detail="Author not found")

@app.post("/author/", response_model=Author, tags=["author"])
def create_author(author: Author):
    author_list.append(author)
    return author

@app.put("/author/{attribute_id}/", response_model=Author, tags=["author"]) 
def change_author(attribute_id : str, updated_author: Author): 
    for index, author in enumerate(author_list): 
        if author.id_to_change == attribute_id:
            author_list[index] = updated_author
            return updated_author
    raise HTTPException(status_code=404, detail="Author not found")

@app.patch("/author/{attribute_id}/{attribute_to_change}", response_model=Author, tags=["author"])
def update_author(attribute_id : str,  attribute_to_change: str, updated_data: str):
    for author in author_list:
        if author.id_to_change == attribute_id:
            if hasattr(author, attribute_to_change):
                setattr(author, attribute_to_change, updated_data)
                return author
            else:
                raise HTTPException(status_code=400, detail=f"Attribute '{attribute_to_change}' does not exist")
    raise HTTPException(status_code=404, detail="Author not found")

@app.delete("/author/{attribute_id}/", tags=["author"])
def delete_author(attribute_id : str):   
    for index, author in enumerate(author_list):
        if author.id_to_change == attribute_id:
            author_list.pop(index)
            return {"message": "Item deleted successfully"}
    raise HTTPException(status_code=404, detail="Author not found") 

############################################
#
#   Library functions
#
############################################
@app.get("/library/", response_model=List[Library], tags=["library"])
def get_library():
    return library_list

@app.get("/library/{attribute_id}/", response_model=Library, tags=["library"])
def get_library(attribute_id : str):   
    for library in library_list:
        if library.id_to_change== attribute_id:
            return library
    raise HTTPException(status_code=404, detail="Library not found")

@app.post("/library/", response_model=Library, tags=["library"])
def create_library(library: Library):
    library_list.append(library)
    return library

@app.put("/library/{attribute_id}/", response_model=Library, tags=["library"]) 
def change_library(attribute_id : str, updated_library: Library): 
    for index, library in enumerate(library_list): 
        if library.id_to_change == attribute_id:
            library_list[index] = updated_library
            return updated_library
    raise HTTPException(status_code=404, detail="Library not found")

@app.patch("/library/{attribute_id}/{attribute_to_change}", response_model=Library, tags=["library"])
def update_library(attribute_id : str,  attribute_to_change: str, updated_data: str):
    for library in library_list:
        if library.id_to_change == attribute_id:
            if hasattr(library, attribute_to_change):
                setattr(library, attribute_to_change, updated_data)
                return library
            else:
                raise HTTPException(status_code=400, detail=f"Attribute '{attribute_to_change}' does not exist")
    raise HTTPException(status_code=404, detail="Library not found")

@app.delete("/library/{attribute_id}/", tags=["library"])
def delete_library(attribute_id : str):   
    for index, library in enumerate(library_list):
        if library.id_to_change == attribute_id:
            library_list.pop(index)
            return {"message": "Item deleted successfully"}
    raise HTTPException(status_code=404, detail="Library not found") 



############################################
# Maintaining the server
############################################
if __name__ == "__main__":
    import uvicorn
    openapi_schema = app.openapi()
    output_dir = os.path.join(os.getcwd(), 'output')
    os.makedirs(output_dir, exist_ok=True)
    output_file = os.path.join(output_dir, 'openapi_specs.json')
    print(f"Writing OpenAPI schema to {output_file}")
    with open(output_file, 'w') as file:
        json.dump(openapi_schema, file)
    uvicorn.run(app, host="0.0.0.0", port=8000)



When you run the code generated, the OpenAPI specifications will be generated:

{
"openapi": "3.1.0",
"info": {
    "title": "FastAPI",
    "version": "0.1.0"
},
"paths": {
    "/author/": {
        "get": {
            "tags": [
                "author"
            ],
            "summary": "Get Author",
            "operationId": "get_author_author__get",
            "responses": {
                "200": {
                    "description": "Successful Response",
                    "content": {
                        "application/json": {
                            "schema": {
                                "items": {
                                    "$ref": "#/components/schemas/Author"
                                },
                                "type": "array",
                                "title": "Response Get Author Author Get"
                            }}}}}}}}}

Note

The full OpenAPI specification is available at http://localhost:8000/docs when you run the generated FastAPI application with uvicorn rest_api:app.

Association classes

When a binary association carries an association class (e.g. Booking — Room through ReservedRoom with attributes agreed_price and additional_charges), the generated API manages the link through the association class instead of a plain join table:

  • The Create/Update payload of each navigable end carries link objects instead of bare ids: "rooms": [{"target": 101, "agreed_price": 95.0, "additional_charges": 5.0}] (target is the primary key of the linked entity; plain ids are still accepted, in which case the association attributes are left unset). To-one ends through an association class are optional at creation time — the link can be established later from the other side or via the association class’s own endpoint.

  • PUT reconciles links: removed targets delete the association row, new targets create one, and kept targets have their association attributes updated.

  • GET ...?detailed=true additionally returns <end>_links with the full association rows.

  • The association class gets its own endpoints; its Create model carries one id field per association end plus the association attributes, and single-item routes are keyed by both foreign keys.

  • Primary keys are respected everywhere: a class whose id attribute is not named id (e.g. Room.number) is queried by its actual primary key.