Updated 26 June 2026
In this blog, we will learn how to build a POS App with Node.js using Medusa JS and Flutter.
A Point of Sale (POS) application is an essential tool for retail stores, restaurants, supermarkets, and businesses that need to manage sales, inventory, customers, and payments efficiently.
Modern POS systems help businesses streamline checkout operations, track inventory in real time, generate sales reports, and manage customer information from a centralized platform.
A Point of Sale (POS) application is software that enables businesses to process customer purchases, manage inventory, track sales, and generate reports.
A POS system typically includes:
Medusa offers modular commerce APIs, workflows, and extensibility that simplify POS development.
Learn more about Medusa in its official documentation.
The POS application communicates with the Medusa backend through custom POS APIs.
The backend handles:
The Flutter mobile application sends API requests to the backend and receives JSON responses that can be rendered in the POS interface.
Medusa provides a flexible backend structure that allows developers to easily build and customize POS features such as product management, barcode scanning, checkout, and inventory tracking.
Inventory levels are automatically updated after every completed transaction, ensuring accurate stock visibility.
A single Medusa backend can power Android, iOS, web, and desktop POS applications simultaneously.
The POS application can include the following features:
We will build a simple cashier-focused POS workflow consisting of:
Custom APIs for:
A responsive mobile application that:
In this section, we will create the APIs required to retrieve product data and perform barcode-based product lookups.
Create a new Medusa application:
|
1 |
npx create-medusa-app@latest pos-backend |
Navigate to the project:
|
1 |
cd pos-backend |
Medusa automatically sets up the required commerce modules and database configuration.
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
import { MedusaRequest, MedusaResponse, } from "@medusajs/framework/http"; export async function GET( req: MedusaRequest, res: MedusaResponse ) { const catalog = [ { id: "prod_1", title: "Medusa T-Shirt", sku: "MEDUSA-TSHIRT-M", price: 15.00, barcode: "123456789012", }, ]; res.json({ catalog }); } |
This endpoint returns product information that can be displayed in the POS application, including product name, SKU, price, and barcode details.
|
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 |
import { MedusaRequest, MedusaResponse, } from "@medusajs/framework/http"; export async function GET( req: MedusaRequest, res: MedusaResponse ) { const { barcode } = req.query; if (!barcode) { return res.status(400).json({ message: "Barcode query parameter is required" }); } // Lookup the specific variant matching the scanned barcode. // In a production application, query the Medusa Product Module. const variant = { id: "variant_1", title: "M / White", product_id: "prod_1", sku: "MEDUSA-TSHIRT-M", price: 15.00, barcode: barcode }; res.json({ variant }); } |
This endpoint accepts a barcode and returns the matching product variant from the Medusa catalog.
In this section, we will connect the application to the Medusa backend, implement barcode scanning, and display product information for cashiers.
Create a new Flutter project:
|
1 |
flutter create pos_mobile_app |
|
1 |
cd pos_mobile_app |
Add required packages to pubspec.yaml:
|
1 2 3 4 5 6 |
dependencies: flutter: sdk: flutter dio: ^5.9.0 mobile_scanner: ^6.0.11 |
Install packages:
|
1 |
flutter pub get |
|
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 |
import 'package:dio/dio.dart'; class CatalogRemoteDataSource { const CatalogRemoteDataSource(this._dio); final Dio _dio; // Retrieve the POS product catalog Future<List<Map<String, dynamic>>> fetchCatalog({String query = ''}) async { final response = await _dio.get<Map<String, dynamic>>( '/pos/catalog', queryParameters: <String, dynamic>{ 'limit': 250, if (query.trim().isNotEmpty) 'q': query.trim(), }, ); final catalog = response.data?['catalog'] as List<dynamic>? ?? const <dynamic>[]; return catalog.map((item) => Map<String, dynamic>.from(item as Map)).toList(); } // Lookup scanned barcode in Medusa JS Future<Map<String, dynamic>?> lookupBarcode(String barcode) async { final response = await _dio.get<Map<String, dynamic>>( '/pos/catalog/barcode', queryParameters: <String, dynamic>{'barcode': barcode}, ); final variant = response.data?['variant']; return variant != null ? Map<String, dynamic>.from(variant as Map) : null; } } |
Create a barcode scanner screen using the mobile_scanner
|
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 |
import 'package:flutter/material.dart'; import 'package:mobile_scanner/mobile_scanner.dart'; class BarcodeScannerScreen extends StatefulWidget { const BarcodeScannerScreen({super.key}); @override State<BarcodeScannerScreen> createState() => _BarcodeScannerScreenState(); } class _BarcodeScannerScreenState extends State<BarcodeScannerScreen> { bool _handled = false; @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: const Text('Scan Barcode'), ), body: MobileScanner( onDetect: (capture) { if (_handled) return; // Prevent StateError by safely checking if barcodes list is empty final code = capture.barcodes.isEmpty ? null : capture.barcodes.first.rawValue; if (code != null && code.isNotEmpty) { _handled = true; // Pop the scanned value back to the previous screen Navigator.pop(context, code); } }, ), ); } } |
In the cashier catalog view, add a scan action button that opens the scanner and adds the resolved product to the cart.
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
// Launch scanner screen and wait for the code final scannedBarcode = await Navigator.push<String>( context, MaterialPageRoute(builder: (_) => const BarcodeScannerScreen()), ); if (scannedBarcode != null) { // Call the Medusa API to resolve the barcode final productVariant = await catalogDataSource.lookupBarcode(scannedBarcode); if (productVariant != null) { // Add the found variant to the cashier shopping cart cartBloc.add(CartItemAdded(productVariant)); } else { ScaffoldMessenger.of(context).showSnackBar( const SnackBar(content: Text('Product not found for this barcode.')), ); } } |
Access the complete product catalog, search items, and filter by categories.
Manage active carts, attach customer details, apply discounts, and process payments.
Scan product barcodes with the camera for instant lookup and fast checkout.



Medusa JS provides a flexible foundation for building a POS App with Node.js, offering features such as product management, barcode scanning, and inventory tracking.
This approach simplifies POS development and supports applications across multiple platforms.
You can also explore other informative blogs on Mobikul for more knowledge.
If you have more details or questions, you can reply to the received confirmation email.
Back to Home
Be the first to comment.