@boarteam/fix
v0.4.0
Published
Dictionary-driven FIX protocol toolkit: parse, validate, and encode FIX messages. Zero runtime dependencies; runs in the browser and Node.
Downloads
364
Readme
@boarteam/fix
A dictionary-driven FIX protocol toolkit for TypeScript — parse, validate, and encode
FIX messages with zero runtime dependencies, in the browser
or Node. This is the engine; pair it with a dictionary such as
@boarteam/fix-dict-fix44.
On a 0.x line: the foundation is solid and well-tested, and the API may still refine ahead of 1.0. See the project README for coverage, testing, and the roadmap — feedback welcome.
import { createFixEngine } from '@boarteam/fix';
import { dictionary } from '@boarteam/fix-dict-fix44';
const fix = createFixEngine(dictionary);
const { message, issues } = fix.parse(raw); // never throws; issues is FixIssue[]
const problems = fix.validate(message); // presence, enums, datatypes, conditional rules
const wire = fix.encode({
msgType: 'D',
fields: {/* ... */},
}); // ordered + framed- Stateless analyzer, not a session/transport engine: no sockets, sequence numbers, or heartbeats — just the protocol.
- Pure & deterministic; never throws on the analyze path (diagnostics are returned
data); browser + Node via
TextEncoder/TextDecoder.
Typed, self-rendering messages
encode(EncodeMessage) is the low-level primitive — an untyped, tag-keyed bag. For a
statically-typed encode side, the dictionary packages ship a message factory generated
from the same dictionary: creating a message for a MsgType yields a builder that knows only
that message's fields/groups and their value types, and renders byte-identical to encode.
import { message, MsgType } from '@boarteam/fix-dict-fix44';
const wire = message(MsgType.MarketDataSnapshotFullRefresh) // typed to this message's body
.set('MDReqID', 'req-1')
.set('Symbol', 'EURUSD')
.set('NoMDEntries', [
{ MDEntryType: '0', MDEntryPx: '1.1050' }, // group entries are typed too
{ MDEntryType: '1', MDEntryPx: '1.1052' },
])
.render({
SenderCompID: 'ME',
TargetCompID: 'YOU',
MsgSeqNum: 1,
SendingTime: '20260716-12:00:00',
});- Value types come from the field datatype: enumerated → the generated value union
(
MDEntryType);Boolean→boolean; numeric (int/float/Price/Qty/…) →number | string(the string is the exact-formatting escape hatch — anumberfloat is rendered byString(), so pass a string when you need an exact form like1.10); everything else →string. Illegal fields, wrong value types, and malformed group entries are compile errors. - Transport/session-agnostic: envelope fields (
MsgSeqNum/SenderCompID/SendingTime/TargetCompID; framing8/9/10are computed) are supplied torender(envelope)— the library never holds a sequence counter, clock, or comp-IDs. The body type excludes them. - Mutable or immutable:
message(...)is a fast, fluent mutable builder for hot loops;message.immutable(...)(and.toImmutable()) is copy-on-write. Both accept a complete bulk init (message('A', { EncryptMethod: 0, HeartBtInt: 30 })— required keys must be named;.assign({...})stays partial) and double as a typed read model (msg.get('Symbol'),msg.get('NoMDEntries')?.[0]?.MDEntryPx) for deriving log metadata. - Absent values pass through: an init/
setvalue ofundefined,null, or''is skipped at render and reads as unset —message('A', { …, Username: username })needs noif (username)guard, in the body, in group entries, and in the envelope alike. A required key can be deliberately left off the wire by naming it withundefined(dialect quirks); to force a literal empty value onto the wire, drop to theencodeprimitive. - The engine façade mirrors it:
createFixEngine<MessageBodies>(dictionary).create(msgType).
Narrowing an unknown message. At a generic boundary — a send(message: MessageView<any>),
a log-metadata helper — the concrete MsgType is erased, so message.msgType === 'W' cannot
narrow the body. A Bodies-bound type guard keyed on msgType restores it: the runtime is a
plain string compare, the typing comes from the Bodies registry. The engine binds one as
engine.is; the dict packages re-export a ready-made isMessageType (and a MessageOf<M> alias
for annotations), so no engine instance is needed. messageTypeGuard<Bodies>() builds a standalone
guard.
import { isMessageType, MsgType } from '@boarteam/fix-dict-fix44';
import type { MessageView } from '@boarteam/fix';
function logMeta(message: MessageView<any>) {
if (isMessageType(message, MsgType.MarketDataSnapshotFullRefresh)) {
// message: MessageView<MarketDataSnapshotFullRefreshBody> — get() is typed, no casts
const securityID = message.get('SecurityID'); // string | undefined
for (const entry of message.get('NoMDEntries') ?? []) {
entry.MDEntryType; // MDFullGrp_NoMDEntriesEntry — entry fields typed
}
}
}Both flavours narrow to the read surface MessageView (the interface the mutable and immutable
message share), not to the mutable/immutable kind — the guard's purpose is typed reads, not
re-obtaining a builder.
Venue extensions (e.g. a broker's custom tags in an existing group) need no regenerated
dictionary: extendDictionary(dictionary, ext) adds them at runtime, and the generated,
per-container group/component interfaces (SecListGrp_NoRelatedSymEntry, …) are augmentable —
patch the shared interface via declaration merging (single venue) or an override-interface
intersection (multiple venues), then rebind messageFactory<MessageBodies>(extended). The
augmented type and the extended dictionary ship as a pair.
Full docs, examples, and the contribution guide are in the monorepo.
FIX 5.0 SP2 / FIXT.1.1
FIX 5.0 splits the wire into a session protocol (tag 8 carries FIXT.1.1) and an
application version negotiated via DefaultApplVerID(1137) / per-message ApplVerID(1128).
The engine models that split first-class — every codec entry point accepts either a single
dictionary or a transport/application pair:
import { createFixEngine } from '@boarteam/fix';
import { dictionary as fixt11 } from '@boarteam/fix-dict-fixt11';
import { dictionary as fix50sp2 } from '@boarteam/fix-dict-fix50sp2';
const fix = createFixEngine({ transport: fixt11, app: fix50sp2 });
const { message } = fix.parse(raw); // 8=FIXT.1.1 frames, SP2 bodies
for (const issue of fix.validate(message)) {
// issue.layer: 'session' findings → answer with a Reject(3);
// 'application' findings → a BusinessMessageReject(j).
}Session (admin) messages are transport-owned — an application-only field on one is flagged
validate/field-outside-layer — and multi-version sessions route per message through an
optional resolveApp(applVerID) hook with a caller-supplied defaultApplVerID (the engine
holds no session state). Prefer a single dictionary? @boarteam/fix-dict-fix50sp2 is the
pair pre-merged (a drop-in for createFixEngine(dictionary)), and
mergeFixtDictionaries(transport, app) builds the same shape from your own app-layer
dialect.
License
Apache-2.0 © Boar Team. See NOTICE. "FIX" is a trademark of FIX
Protocol Limited; this is an independent project, not affiliated with or endorsed by it.
