How to build your own LocalStack Extension
LocalStack Extensions can facilitate specialized workflows or development needs. This guide walks through using LocalStack's Extensions API to create a mock API for Anthropic, a popular LLM provider, that allows you to test your application code without relying on the live service.
Introduction
LocalStack allows developers to develop and test cloud applications locally, but it goes beyond emulating AWS or Snowflake services. It can also be extended with custom functionality to support workflows or development needs that don’t fit into the standard cloud service model. LocalStack Extensions make this possible: they are pluggable Python distributions that run alongside the LocalStack runtime in a Docker container.
Through the Extension API, you can hook into different lifecycle phases of LocalStack, execute custom code, or add custom routes to LocalStack’s HTTP gateway with your own server-side logic.
In this tutorial, we’ll build a LocalStack Extension that mocks Anthropic’s Messages API. The mock generates fake responses that the Anthropic Python SDK can parse, so you can test your Anthropic integration code locally without real API keys or network calls. Anthropic is just the example here; the same approach works for mocking any third-party API.
Key Concepts
LocalStack Extensions API
Extensions are Python packages that LocalStack loads at startup. Each extension is a class inheriting from Extension that can override these lifecycle hooks:
on_extension_load(): runs when LocalStack loads the extensionon_platform_start(): runs when LocalStack begins its startup sequenceon_platform_ready(): runs after LocalStack is fully initializedupdate_gateway_routes(): adds custom HTTP routes to LocalStack’s gateway
The update_gateway_routes hook is how we’ll serve a mock API. You register route handlers, and LocalStack serves them on its gateway (port 4566 by default), alongside the emulated AWS services.
How the mock works
The Anthropic Python SDK supports overriding the base URL when creating a client. We’ll point the SDK at our LocalStack extension instead of the real Anthropic API. The extension exposes a /v1/messages endpoint that returns JSON matching the expected schema, and the SDK parses it as if it came from the real service.
Prerequisites
localstackCLI with a LocalStack Auth Token- Docker
- Python 3.8+ and
pip - Cookiecutter (
pip install cookiecutter)
You’ll also need a PyPI account and API token if you plan to publish the extension.
Step 1: Scaffold the extension project
The LocalStack CLI includes a developer toolkit for extensions. Generate a new project from the official template:
pip install cookiecutterlocalstack extensions dev newThe command prompts for a few fields. Here’s what I entered:
[1/9] project_name (My LocalStack Extension): LocalStack Extension for Anthropic[2/9] project_short_description (All the boilerplate you need to create a LocalStackextension.): LocalStack Extension for Anthropic[3/9] project_slug (localstack-extension-for-anthropic): localstack-extension-anthropic[4/9] module_name (localstack_extension_anthropic): localstack_anthropic[5/9] class_name (LocalstackExtensionForAnthropic): LocalstackAnthropicExtension[6/9] full_name (Jane Doe): Harsh Mishra[7/9] email (jane@example.com): harsh@localstack.cloud[8/9] github_username (janedoe): HarshCasper[9/9] version (0.1.0):You’ll get the following project structure:
localstack-extension-anthropic├── Makefile├── README.md├── localstack_anthropic│ ├── __init__.py│ └── extension.py└── pyproject.tomlInstall the project dependencies:
cd localstack-extension-anthropicmake installA virtual environment is created under .venv/ and the extension is installed in editable mode.
Step 2: Start the extension in developer mode
Developer mode mounts your local extension directory into the LocalStack container, so you can edit code and restart without rebuilding anything. Enable it from inside the project directory:
localstack extensions dev enable ./Then start LocalStack:
EXTENSION_DEV_MODE=1 localstack startThe logs should show:
──────────────────── LocalStack Runtime Log (press CTRL-C to quit) ────────────────────==================================================👷 LocalStack extension developer mode enabled 🏗- mounting extension /opt/code/extensions/localstack-extension-anthropicResuming normal execution, ...==================================================The generated extension.py has a basic skeleton:
from localstack.extensions.api import Extension, http, aws
class LocalstackAnthropicExtension(Extension): name = "localstack-extension-anthropic"
def on_extension_load(self): print("MyExtension: extension is loaded")
def on_platform_start(self): print("MyExtension: localstack is starting")
def on_platform_ready(self): print("MyExtension: localstack is running")
def update_gateway_routes(self, router: http.Router[http.RouteHandler]): pass
def update_request_handlers(self, handlers: aws.CompositeHandler): pass
def update_response_handlers(self, handlers: aws.CompositeResponseHandler): passTo verify the extension loads correctly, modify on_platform_ready to use proper logging:
import logging
LOG = logging.getLogger(__name__)
class LocalstackAnthropicExtension(Extension): name = "localstack-extension-anthropic"
# ...other methods...
def on_platform_ready(self): LOG.info("my plugin is loaded and localstack is ready to roll!")Restart LocalStack (localstack restart) and check the logs. You should see:
2026-04-01T11:39:44.415 INFO --- [ady_monitor)] l.extension : my plugin is loaded and localstack is ready to roll!Ready.Step 3: Implement the Anthropic Messages API
Anthropic’s Messages API accepts a POST to /v1/messages with a list of messages and returns a structured response. A typical request through the Python SDK:
import anthropic
client = anthropic.Anthropic()message = client.messages.create( model="claude-sonnet-4-5-20250929", max_tokens=1024, messages=[ {"role": "user", "content": "Hello, world"} ])The response JSON looks like:
{ "content": [ { "text": "Hi! My name is Claude.", "type": "text" } ], "id": "msg_013Zva2CMHLNnXjNJJKqJ2EF", "model": "claude-sonnet-4-5-20250929", "role": "assistant", "stop_reason": "end_turn", "stop_sequence": null, "type": "message", "usage": { "input_tokens": 25, "output_tokens": 15 }}The important detail: content is a list of content blocks (each with a type and text field), not a plain string. Our mock needs to match this structure or the SDK will fail to deserialize it.
Create localstack_anthropic/mock_anthropic.py:
import jsonimport logging
from faker import Fakerfrom rolo import Request, Response, route
faker = Faker()LOG = logging.getLogger(__name__)
class Api: @route("/v1/messages", methods=["POST"]) def complete(self, request: Request): data = request.get_data() req = json.loads(data)
user_message = next( (msg["content"] for msg in req.get("messages", []) if msg["role"] == "user"), "", ) model = req.get("model", "claude-sonnet-4-5-20250929")
if not user_message: return Response( json.dumps({"error": "Missing user message"}), content_type="application/json", status=400, )
completion_text = faker.sentence(nb_words=12) fake_id = f"msg_{faker.hexify(text='^^^^^^^^^^^^^^^^^^^^^^^^^^^^')}"
response_data = { "content": [{"type": "text", "text": completion_text}], "id": fake_id, "model": model, "role": "assistant", "stop_reason": "end_turn", "stop_sequence": None, "type": "message", "usage": { "input_tokens": faker.random_int(min=10, max=50), "output_tokens": faker.random_int(min=10, max=100), }, }
return Response(json.dumps(response_data), content_type="application/json")We’re using Rolo for route handling (a Python HTTP framework on top of Werkzeug, used internally by LocalStack) and Faker to generate random response text. The content field is returned as a list of TextBlock-style objects to match the Anthropic SDK’s expected format.
Step 4: Wire the mock into LocalStack’s gateway
Replace the contents of localstack_anthropic/extension.py to register the mock API as gateway routes:
import logging
from localstack import configfrom localstack.extensions.api import Extension, httpfrom rolo.router import RuleAdapter, WithHostfrom werkzeug.routing import Submount
LOG = logging.getLogger(__name__)
class LocalstackAnthropicExtension(Extension): name = "localstack-extension-anthropic"
submount = "/_extension/anthropic" subdomain = "anthropic"
def on_extension_load(self): logging.getLogger("localstack_anthropic").setLevel( logging.DEBUG if config.DEBUG else logging.INFO )
def on_platform_start(self): LOG.info("Anthropic extension: LocalStack is starting")
def on_platform_ready(self): LOG.info("Anthropic extension: LocalStack is running")
def update_gateway_routes(self, router: http.Router[http.RouteHandler]): from localstack_anthropic.mock_anthropic import Api
api = RuleAdapter(Api())
router.add( [ Submount(self.submount, [api]), WithHost( f"{self.subdomain}.{config.LOCALSTACK_HOST.host}<__host__>", [api], ), ] ) LOG.info( "Anthropic mock available at %s%s", str(config.LOCALSTACK_HOST).rstrip("/"), self.submount, ) LOG.info( "Anthropic mock available at %s", f"{self.subdomain}.{config.LOCALSTACK_HOST}", )The mock Api class gets registered on two paths:
- Submount at
localhost.localstack.cloud:4566/_extension/anthropic(path prefix) - Subdomain at
anthropic.localhost.localstack.cloud:4566(works well as abase_urlfor SDKs)
Step 5: Add the Faker dependency
Open pyproject.toml and add faker to the dependencies list:
dependencies = [ "faker>=30.0.0"]Then rebuild and restart:
make clean && make installlocalstack restartCheck the LocalStack logs. You should see:
2026-04-01T11:39:40.042 INFO --- [ MainThread] l.extension : Anthropic mock available at localhost.localstack.cloud:4566/_extension/anthropic2026-04-01T11:39:40.042 INFO --- [ MainThread] l.extension : Anthropic mock available at anthropic.localhost.localstack.cloud:4566Step 6: Test the mock with curl
Send a request to the subdomain route:
curl -s -X POST http://anthropic.localhost.localstack.cloud:4566/v1/messages \ -H "Content-Type: application/json" \ -H "x-api-key: test" \ -d '{"model":"claude-sonnet-4-5-20250929","max_tokens":1024,"messages":[{"role":"user","content":"Hello, Claude"}]}'Expected output (the text will differ since Faker generates random sentences):
{ "content": [ { "type": "text", "text": "Minute middle medical start fight well heavy poor camera those usually." } ], "id": "msg_1e00935c3e874b49ef943da4b752", "model": "claude-sonnet-4-5-20250929", "role": "assistant", "stop_reason": "end_turn", "stop_sequence": null, "type": "message", "usage": { "input_tokens": 18, "output_tokens": 12 }}The submount path works the same way:
curl -s -X POST http://localhost.localstack.cloud:4566/_extension/anthropic/v1/messages \ -H "Content-Type: application/json" \ -H "x-api-key: test" \ -d '{"model":"claude-sonnet-4-5-20250929","max_tokens":1024,"messages":[{"role":"user","content":"Hello"}]}'Step 7: Test with the Anthropic Python SDK
Create anthropic_test.py in the project root:
from anthropic import Anthropic
client = Anthropic( base_url="http://anthropic.localhost.localstack.cloud:4566", api_key="test")
message = client.messages.create( max_tokens=1024, messages=[ { "role": "user", "content": "Hello, Claude", } ], model="claude-sonnet-4-5-20250929",)
print(message.content[0].text)We set base_url to the subdomain route and api_key to any non-empty string (the mock doesn’t check auth).
Install the Anthropic SDK and run it:
pip install anthropicpython3 anthropic_test.pyOutput:
Administration feel quite provide interesting loss type.The text is random Faker output, which is expected. What matters is that the SDK deserialized the response without errors: the Message object, TextBlock content, and all fields came through correctly.
Step 8: Publish the extension (optional)
To share the extension, publish it to PyPI. The generated Makefile includes a publish target:
make publishYou’ll need a PyPI API token configured for twine.
Once published, others can install it with:
localstack extensions install localstack-extension-anthropicOr from a Git repository directly:
localstack extensions install "git+https://github.com/harshcasper/localstack-extension-anthropic.git#egg=localstack-extension-anthropic"You can also add it to the Extensions Library on the LocalStack Web Application for a shareable install link.
Conclusion
We built a LocalStack Extension that mocks Anthropic’s Messages API. The same pattern works for any third-party HTTP API: define a Rolo route handler, wire it into the gateway through update_gateway_routes, and point your SDK at the LocalStack endpoint.
The mock here is intentionally bare-bones. You could extend it to handle streaming responses, message batches, or return proper error codes. Returning deterministic responses based on the input prompt would also be useful for integration tests.
For more extension examples and templates (including a React UI template), check the LocalStack Extensions repository.