Vol. 24 · Application Engineering

Mini-Repository: FastAPI + LangGraph + LangChain as a Service

This volume does not answer the question “how to write another agent”, but the question “how to make a live external application”. At the center is one solid Python mini-repo: ‘FastAPI’ as a public HTTP/SSE layer, ‘LangGraph’ as runtime, ‘LangChain’ as a layer of models, tools and middleware, external persistence for threads, scaling, auth, health/readiness and a separate place for ‘MCP’ as a tool/data plane.

01

The Big Picture of Service

5 cards
01ArchitecturePublic application = ‘FastAPI’ outside, ‘LangGraph’ inside, ‘LangChain’ as a layer of components

If you remove the excess magic, the external LLM microservice looks like a normal backend. FastAPI accepts queries and gives answers/streams. LangGraph manages stateful execution: threads, routing, interrupts, checkpoints. LangChain provides models, messages, tools, middleware and structured output. Everything else is the infrastructure around this core.

Diagramtext
External layer and internal RUNTIME

browser / frontend / other service

        FastAPI
  routes / auth / schemas / stream

      Agent Service

       LangGraph
 state / checkpoints / interrupts

      LangChain
 model / tools / middleware

 DB / vector DB / queue / object storage / MCP / external APIs
02ArchitectureWhat makes a service a “microservice”

It's not code size or Docker. Microservice is made by an external contract, a separate deploy lifecycle, its own health/readiness rules, its own persistence logic and the ability to safely serve customers from the outside as an independent service.

minimum set

  • stable API
  • separate deploy
  • state/storage
  • auth / monitoring / retries
03ArchitectureThis service usually has three modes of interaction.

Almost all production scenarios fit into three forms: the usual synchronous request/response, streaming response, and long-lived flow with interrupt/resume or background processing. If you think through all three modes in advance, the architecture becomes more sustainable.

three modestext
1. sync:
   I have been waiting for JSON to come back.

2. stream:
   request → on the way to give tokens / updates

3. durable:
   Request → saved state → pause / worker / resume later
04ArchitectureOutside, it is better to expose business operations, not internal node-s of the count.

The client doesn’t usually need to know which “retrieve node”, “review node” or “tool router” you have. Externally, it is better to give APIs like "chat", "resume review", "search knowledge", "summarize document". The internal graph remains your implementation, not part of a public contract.

signal

  • API = business activities
  • API = internal node names
05ArchitectureThread id is quickly becoming part of an external API.

As soon as the service supports multi-turn, streaming history or ‘interrupt/resume’, the outside world starts to refer to specific threads. This means that ‘thread id’, ‘run id’ or their counterparts should be designed consciously, rather than generating randomly ‘inside’.

practicetext
client sends:
  thread_id? maybe existing

service decides:
  new thread or continue old thread

response returns:
  thread_id
  maybe interrupt payload
  maybe run metadata
02

One Whole Mini-Repo

5 cards
06RepoOne of the normal layouts for this repository

The purpose of the structure is to separate the public API, LangGraph runtime, LangChain components and infrastructure. Then the service remains readable and does not turn into one giant 'main.py' with routes, prompts, tools and SQL in a jumble.

Diagramtext
REPO TREE

app/
  main.py
  core/
    settings.py
    security.py
    observability.py
  api/
    deps.py
    routers/
      chat.py
      resume.py
      health.py
  agent/
    contracts/
      state.py
      context.py
      schemas.py
    components/
      models.py
      prompts.py
      tools.py
      middleware.py
      mcp_clients.py
    graph/
      nodes.py
      routes.py
      build.py
    services/
      chat_service.py
      stream_service.py
  infra/
    persistence.py
    queue.py
    mcp_server.py
tests/
Dockerfile
pyproject.toml
07Repo'main.py' must raise resources once through 'lifespan'

Graph, checkpointer, store, database pool, model clients and service objects are better collected at the start of the application, not for every request. For this, FastAPI has a ‘lifespan’, and it is one of the most important practical pieces for a production service.

skeletonpython
@asynccontextmanager
async def lifespan(app: FastAPI):
    settings = load_settings()
    checkpointer, store = await build_persistence(settings)
    graph = build_graph(checkpointer=checkpointer, store=store)
    app.state.chat_service = ChatService(graph=graph, settings=settings)
    yield

app = FastAPI(lifespan=lifespan)
08Repo‘api/routers/*.py’ – a thin layer above services

An HTTP route does not need to know how a graph works. Its job is simpler: fail the input, pull out the auth/tenant context, invoke the service method, and return the JSON or stream. This keeps the HTTP code small and replaceable.

good way

  • schema validation
  • auth dependency
  • service call
  • complex orchestration logic
09Repo‘agent’ is the heart of the product, not the technical folder

Inside the “agent” live domain contracts, models, tools, middleware, graph nodes and service orchestration. This folder is responsible for the business behavior of the application. This is where most of the real value of the service is concentrated.

logictext
contracts   -> state / context / response schema
components  -> model / tools / middleware / prompt layer
graph       -> nodes / routes / build
services    -> invoke / stream / resume orchestration
10Repo‘infra/’ is best highlighted explicitly because production dependencies live there.

Persistence, queue, MCP server/client glue, metrics, logging setup, and other operational code are growing rapidly. If you do not take them into a separate layer, they will mix with the graph code and begin to complicate both the development and the depot.

often lives infra

  • checkpointer/store builders
  • queue adapters
  • MCP server bootstrap
  • logging/tracing setup
03

Public HTTP API

6 cards
11HTTP API‘POST /v1/chat’ is the most understandable sync input

If the user needs a regular response without streaming, it is convenient to have one sync endpoint. Inside, it builds a 'thread id', runtime context, calls 'graph.invoke(...)' through the service, and returns a validated 'response model' along with metadata.

examplepython
@router.post("/v1/chat", response_model=ChatResponse)
async def chat(req: ChatRequest, svc = Depends(get_chat_service), ctx = Depends(get_request_context)):
    return await svc.chat(req, ctx)
12HTTP APIStreaming is a separate endpoint rather than a “magic option”.

LangGraph can stream tokens and state updates. Outwardly, it is convenient to give it through “StreamingResponse”, usually in the format of “text/event-stream” or chunked JSON lines. A separate endpoint makes the contract clearer and doesn’t force sync clients to understand the streaming protocol.

examplepython
@router.post("/v1/chat/stream")
async def chat_stream(req: ChatRequest, svc = Depends(get_stream_service), ctx = Depends(get_request_context)):
    return StreamingResponse(
        svc.stream(req, ctx),
        media_type="text/event-stream",
    )
13HTTP APIIf there is an ‘interrupt’, there is a ‘resume’ endpoint.

This is one of the most common flaws. People add approval-node with ‘interrupt()’, but forget to design the external path of continuation. Production requires a separate endpoint that accepts 'thread id' and payload for 'Command'.

examplepython
@router.post("/v1/threads/{thread_id}/resume")
async def resume(thread_id: str, req: ResumeRequest, svc = Depends(get_chat_service), ctx = Depends(get_request_context)):
    return await svc.resume(thread_id=thread_id, payload=req.payload, ctx=ctx)
14HTTP API`/healthz`, `/readyz`, `/version`, `/openapi.json` are not trifles

A live service needs service entry points. Liveness says “process is alive,” readiness says “you can take traffic,” version says “what’s locked in right now,” OpenAPI says “how customers integrate.” These endpoints are quickly becoming a must for the platform team.

minimum

  • GET /healthz
  • GET /readyz
  • GET /version
  • GET /openapi.json
15HTTP APIAuth, Tenant Context and CORS are better solved at FastAPI

If a service lives on the outside, it almost always needs API keys, bearer tokens or other auth-mechanism. It usually gives rise to the request context: “user id”, “tenant id”, roles, feature flags. For browser clients, CORS is added. All this is more convenient to decorate the dependencies layer from FastAPI.

ideatext
Authorization header
→ verify principal
→ build RequestContext(user_id, tenant_id, scopes)
Put it in the service/graph context
16HTTP API'Idempotency-Key' is especially important if a graph can cause side effects.

This is one of the “non-obvious but highly relevant” production units. If the client repeats the request because of the timeout, and inside the graph sends an email, creates a ticket or writes in CRM, without idempotence, you will easily get duplicate actions.

particularly

  • resume after timeout
  • tool with external side effect
  • repeat client retries
04

LangGraph and LangChain Inside the Service

5 cards
17LangGraph`state.py`, `context.py`, `schemas.py` are the main contracts of the service

The internal runtime should be typed as well as the external HTTP API. “state” describes the living state of the graph, “context” – immutable launch data like “user id” and tenancy, “schemas” – the boundaries of structured output and API. This makes graph code less fragile and easier to test.

statepython
class AgentState(TypedDict):
    messages: Annotated[list[AnyMessage], add_messages]
    retrieved_docs: list[str]
    final_answer: dict | None
    approval_required: bool | None
18LangGraphThe `LangChain' within such a repos are models, tools, middleware and output contracts.

Even if you have orchestration on LangGraph, LangChain is still very appropriate as a layer of finished components. It is home to ‘init chat model’, tools with ‘ToolRuntime’, middleware for model/tool hooks, messages and structured output schemes.

component layertext
model       = init_chat_model(...)
tools       = [search_docs, create_ticket, get_profile]
middleware  = [dynamic_prompt, tool_error_handler]
response_schema = AnswerSchema
19LangGraphThe graph should be compiled once at the start, not for every HTTP request.

`builder.compile(...)' assembles runnable graph with checkpointer and store. If you do this for every call, you’ll get extra complexity, lost productivity, and unstable resource initialization. The service usually receives a compiled graph from lifespan.

build_graphpython
def build_graph(checkpointer, store):
    builder = StateGraph(AgentState)
    builder.add_node("agent", agent_node)
    builder.add_node("review", review_node)
    builder.add_conditional_edges("agent", route_after_agent)
    return builder.compile(checkpointer=checkpointer, store=store)
20LangGraphChatService links HTTP layer and graph runtime

This is the place where HTTP payloads are born ‘inputs’, ‘config’ and runtime ‘context’, and then called ‘graph.invoke(...)’ or ‘graph.astream(...)’. Such a service layer is useful because it is convenient to hold thread policy, metadata, error mapping and response shaping.

logictypescript
config = {"configurable": {"thread_id": thread_id}}
context = RequestContext(...)

result = await graph.ainvoke(
    {"messages": [{"role": "user", "content": req.message}]},
    config=config,
    context=context,
)
21LangGraphInterrupt() is no longer an internal feature of the graph, but a part of the UX and API design.

Once the graph can settle for human approval, the service should be able to beautifully return the payload to the UI or external client. This means a well-thought-out JSON contract: interrupt type, question text, draft answer, acceptable actions, and the way to resume.

come back

  • thread_id
  • interrupt type
  • payload for UI
  • resume action schema
05

State, Scaling and Long Life

5 cards
22DeployIn order for the service to live, the state must be removed from the process.

One of the most important production shifts: InMemorySaver and local dicts are well suited only for laptop and local development. A live service that restarts, scales and maintains multiple instances must store thread state and memory in external persistence.

Diagramtext
Proper separation

app instance A  ─┐
app instance B  ─┼──→ external checkpointer / store / DB
app instance C  ─┘

Then any copy can continue threading thread id
23DeployHorizontal scaling breaks if memory lives only in the workman

If one query hits worker A and the next thread id hits worker B, the in-memory state disappears. Therefore, for multi-worker and multi-replica scripts, checkpointer/store should be shared, and service instances should be as stateless as possible.

red flag

  • thread history only in the RAM process
  • Local disk as the only storage
24DeployNot every LLM-flow needs to live inside an HTTP request lifecycle.

This is another non-obvious, but really important block. If processing can take minutes, call a lot of tools, do reindex or generate a report, it is better to highlight the background job/worker pattern. HTTP endpoint then creates a run and puts the task in a queue, and the client then reads the status or streams.

patterntext
POST /v1/jobs
→ validate request
→ create run record
→ enqueue task
→ return 202 Accepted + run_id

worker
→ loads same graph/service layer
→ executes durable flow
→ writes status/results
25DeployDon’t put important artifacts on a local container disk.

Chats, traces, prompt snapshots, uploaded files, generated reports and other useful artifacts should be stored in an external storage: SQL / NoSQL, object storage, vector DB. The container or process does not have to be the only place where user data lives.

dataplane

  • SQL / Postgres
  • object storage
  • vector DB
  • queue broker
26DeployTimeout budgets, cancellations and backpressure are required in advance.

The real service has limits: upstream LLM timeout, tool timeout, client timeout, ingress timeout, limit on simultaneous streams. If they are not designed in advance, the service will begin to hang, accumulate hanging connections and fall under load.

think through

  • request timeout
  • tool timeout
  • queue depth limit
  • cancel on client disconnect
06

Where is MCP Really Needed?

5 cards
27MCPMCP usually lies next to the microservice, rather than replacing its public API.

One of the most useful shifts in understanding is that FastAPI and MCP are different. A public HTTP API is needed for browsers, backends, and business integrations. MCP is needed where LLM hosts and agent systems want standardized tools, resources, and prompts. It's not the same thing.

Diagramtext
Two adjacent layers

public app API:
  browser / backend / mobile → FastAPI

tool/data plane:
  host / agent platform / IDE → MCP server

Often the same product has both layers.
28MCPPattern A: Your service is an MCP client to an external system n

This is a good pattern if your LangGraph service needs standardized tools from the outside world: GitHub, Slack, files, CRM, internal knowledge, etc. Then your ‘LangChain’ tool layer can run through MCP clients rather than holding dozens of custom SDKs.

what it looks liketext
FastAPI request
→ graph node
→ LangChain tool
→ MCP client
→ remote MCP server
→ tool result back into graph
29MCPPattern B: Your repository exports the MCP server to other agents

If your product itself knows useful operations or has important data, you can add “mcp server.py” and give some of the features out through MCP. Then IDEs, external hosts or other agent platforms will be able to use your tools/resources/prompts in a standard way.

sketchsql
from mcp.server.fastmcp import FastMCP

mcp = FastMCP("my-agent-platform")

@mcp.tool
def search_internal_docs(query: str) -> str:
    ...

@mcp.resource("kb://policy/{name}")
def get_policy(name: str) -> str:
    ...
30MCP'FastAPI' vs 'MCP' vs both at once

It is better not to confuse these interfaces. FastAPI is good as a business API for applications and services. MCP is a standardized tool/data protocol for hosts and agents. Very often, the correct answer is to have both interfaces, but with different audiences and different areas of responsibility.

Interface.Better for you.
FastAPIfrontend, backend, product API, auth, business flows
MCPtools/resources/prompts for hosts and agent systems
BothA platform that people and other agents need.
31MCPMCP has its own security and its own trust boundary.

If you are picking up a remote MCP server, think not only about transport, but also about auth, origin checks, tool descriptions, prompt injection through resources and rights to side-effect tools. An MCP server is a login to your tool/data plane, not just another JSON endpoint.

really important

  • Streamable HTTP transport
  • auth / OAuth
  • least privilege tools
  • prompt injection via tool/resource content
07

Relevant production blocks that are often forgotten

5 cards
32OpsObservability should go all the way: 'request id', 'thread id', 'run id'

Logs “received the request” and “response” are not enough. For an LLM service, it is very useful to carry correlation identifiers through the entire stack: HTTP request id, thread id, run id, tool call id. Then you can associate a specific client failure with a specific graph run and a specific execution tool.

idstext
request_id  -> HTTP request
thread_id   -> conversation / state line
run id -> specific graph launch
tool call id -> external tool invocation
33OpsVersioning not only API, but also graph behavior

Even if the endpoint is called the same, the behavior of the service may change due to a new prompt, new tool rules, different routing logic, or other output schema. Therefore, it is useful to have a clear version of the API, prompt/graph revision and migration discipline for customers.

version

  • HTTP API version
  • response schema
  • graph/prompt revision
34OpsTests are required on three levels: HTTP, service, graph

If you test only the endpoint, it will not be clear if the auth, service wiring, routing graph, or specific tool is broken. Therefore, unit tests tools/middleware, integration tests compiled graph and API tests for schemas, auth and stream/resume endpoints are useful here.

Diagramtext
TEST LAYERS

HTTP tests

service tests

graph integration tests

tool / interrupt / resume tests
35OpsDeploy Path: Docker → reverse proxy / ingress → env-configured workers

For external life, the service is usually containerized, raised behind proxy/ingress with HTTPS, throwing env vars and scale policy. An important engineering idea: once the state is outdoors, the application becomes much easier to scale and survive restarts.

walkwaytext
local dev
→ Docker image
→ staging with real DB/checkpointer
→ prod behind ingress / load balancer
→ replicas + shared persistence

In many clusters, it is easier to scale replicas than to build complex multiprocesses within a single container.
36BridgeThe main summary of the volume

If you compress everything to the most useful engineering framework, the result is: FastAPI gives an external contract, LangGraph gives a long-lasting runtime, LangChain gives convenient building blocks, external persistence makes the service live, and MCP adds a standardized tool/data interface for other hosts and agents if necessary.

Diagramtext
One main picture

public clients

FastAPI API

service layer

LangGraph runtime

LangChain components

external state + tools + MCP + data plane

Such a service can not only be run locally, but also actually operate.

No dead end

Keep moving through the map.

Continue in sequence, switch to a related guide, or return to the seven-track learning map.