Your first service
In the Core NATS chapter you built an inventory responder. It subscribed
to orders.inventory.check, read each request, and published an answer
back. It worked, but it was anonymous: nothing on the network knew its
name, its version, or which subjects it served.
This page promotes that responder into a service: the same answering behavior, now wrapped by the micro framework so it gains a name, a version, and a built-in identity. The bytes on the wire stay the same. The responder becomes a first-class thing the network can name and, on later pages, discover, observe, and scale.
You need a local nats-server running and you should already be
comfortable with request-reply. This
page covers the two things the framework adds on top of how a reply
finds its way back, rather than re-teaching that.
A service is a named responder
A service is a request-reply responder that the framework formalizes.
You create one by calling AddService with three values: a Name, a
Version, and a Description. The framework takes those, opens the
necessary subscriptions, and hands you back a handle to manage it.
The Name is shared by every copy of this service you ever run. Here it
is OrderInventory. The Version is a SemVer string, 1.0.0 for our
first cut. The Description is free text for humans.
When you create the service, the framework also generates a unique
service ID for this one running copy, an auto-generated NUID like
IcpremQQGTYS0fK1iyfQ86. You never set it. The service name
identifies the kind of service; the service ID identifies this one
instance of it. That distinction is what later lets you run five copies
of OrderInventory and still talk to exactly one of them.
A service on its own answers nothing yet. It needs an endpoint: a
named handler bound to a subject. This is where the inventory logic
lives. Add one endpoint, check, on the subject orders.inventory.check
(the same subject the Core NATS responder used) and give it the handler
that answers the in-stock question.
- CLI
#!/bin/bash
# Run the OrderInventory service. In a client library you call
# AddService(Name, Version, Description) and then AddEndpoint to bind a
# handler to a subject. From the CLI, `nats service serve` stands up a
# service for you: it registers a name and version, subscribes to the
# discovery verbs under $SRV, and answers requests on an endpoint.
#
# The endpoint joins the default queue group "q" automatically, so
# running more than one copy load-balances requests across them.
#
# Leave this running in its own terminal. It is now OrderInventory, a
# named service the network can discover, not an anonymous responder.
nats service serve OrderInventory
Two details matter. First, the endpoint joins the framework's
default queue group, "q", automatically. That's the same queue
group mechanism from Core NATS: if you
run more than one copy of this service, each request goes to exactly one
of them. You get load balancing for free, and the scaling
page builds on it.
Second, creating the service quietly subscribes it to a set of
discovery subjects under $SRV: the verbs that let callers find it
and read its stats. You don't see them here; the
discovery page is where they are used. For
now, know that they exist the moment the service starts.
The full set of service configuration fields and their valid ranges is documented in Reference. We only need the behavior here.
The handler contract
Inside an endpoint, the handler is the function that processes a
request. Its contract is the request-reply you already know, with the
framework managing the connection. Each request hands you a request
object. You read the incoming bytes with Data() and you send the answer
with Respond().
That covers the whole shape, Data() to read and Respond() to answer.
The framework took care of subscribing on orders.inventory.check,
joining the queue group, reading the reply subject off the incoming
message, and publishing your answer to it. The handler doesn't touch a
reply subject or an inbox; the framework does, the same work the raw
responder performed manually.
With the service running, a caller asks the same question as before. The
order-svc client sends the canonical order payload to the check
endpoint and waits for one reply:
- CLI
#!/bin/bash
# Ask the OrderInventory service whether an order's item is in stock.
# `nats service request` looks the service up by name, resolves the
# endpoint's subject from its discovery info, sends the order payload,
# and prints the reply.
#
# The arguments are the service name, the endpoint name, and the payload.
# Underneath it is plain request-reply: a private reply subject, the
# request out on the endpoint's subject, and one answer back.
nats service request OrderInventory check \
'{"order_id":"ord_8w2k","customer":"acme-co","total_cents":4200,"ts":"2026-05-22T10:14:22Z"}'
The request travels to orders.inventory.check, the framework routes it
into the check endpoint's queue subscription, your handler runs, and
the response flows back to the caller's reply subject. Here's the round
trip with the framework wrapper in place:
The orange arrow is the request out on orders.inventory.check. The
green arrow is the answer coming back. Underneath, it's plain
request-reply over a queue group; the framework gave the responder a name
and an identity.
Pitfalls
Two mistakes are common when building your first service. Both are about validation: the framework names your responder and sets up its subscriptions, but it doesn't validate the data that flows through it.
Validate the request body; never let bad input crash the handler. The
framework hands your handler whatever bytes the caller sent. If your
handler blindly parses them and the caller sent garbage, the parse fails
and, depending on the language, the handler can panic, return nothing,
or leave the caller waiting out a timeout. Don't assume the payload is
well-formed. Parse it, and on failure respond with a service error
instead: a response that carries Nats-Service-Error and a
Nats-Service-Error-Code header whose value is always safe to parse as a
number (use 400 for bad input). The caller then sees
a clear "bad request" rather than a hang, and the service stays up for
the next caller.
Send a deliberately malformed body and watch the service answer with the error rather than fall over:
- CLI
#!/bin/bash
# Pitfall demo: send a malformed body to the OrderInventory service.
# A handler that validates its input parses req.Data() and, on failure,
# responds with a service error carrying the Nats-Service-Error and
# Nats-Service-Error-Code (400) headers instead of crashing or hanging.
#
# `--raw` prints the reply body untouched. The caller still gets a clear
# answer back, the error headers tell it the request was bad, and the
# service stays up for the next caller.
nats service request OrderInventory check 'not-json' --raw
The full mechanics of reading that error code back live on the observability page; here the point is only that the handler must produce it.
A service Name and Version are validated at creation. The Name
must match the framework's character rule (letters, digits, hyphens,
underscores; no dots or spaces), and the Version must be valid SemVer.
A Name of Order Inventory (with a space) or a Version of v1
(not SemVer) fails the AddService call outright; the service never
starts. Don't discover this in production: pick a valid Name and a
real SemVer Version (1.0.0) the first time. The same goes for any
Metadata you attach. It's immutable once set, so there's no editing
it after creation; you stop the service and start a new one. The full
character rules are in Reference.
Where you are
The Acme world now has its first real service:
OrderInventory, version1.0.0, with a unique service ID assigned by the framework.- One endpoint,
check, answering onorders.inventory.checkin the default queue group"q". - The same in-stock behavior as the Core NATS responder, now named and
wrapped, plus discovery subscriptions running quietly under
$SRV.
Nothing about the payload or the subject changed; the responder became a service.
What's next
One service with one endpoint is the smallest useful shape. Real services expose several endpoints and organize them under subject prefixes called groups. The next page adds a second endpoint and a second service.
Continue to Endpoints and groups.
See also
- Core NATS → Request-reply — how the reply actually finds its way back, which this page assumes.
- Core NATS → Queue groups — the default
"q"group that an endpoint joins. - Reference — every service configuration field and its valid range.