micro
micro is a thin Node.js HTTP service wrapper from Vercel. You export one request handler, and its CLI opens a TCP port, Unix socket, or Windows named pipe; the handler can return a value or write directly to the standard Node response. Helpers buffer JSON, text, and binary bodies, send typed responses, and turn thrown errors into HTTP status codes. It intentionally has no router or middleware system and is designed for containerized single-purpose services, not Vercel serverless functions.
micro is still understandable and useful for an existing one-handler container, but its frozen release, separate development tool, and missing web-framework basics make it a poor default for a new service. Native Node HTTP is nearly as direct, while Hono, Fastify, or Koa add the application structure most teams soon need.
Use it if
- You want one small HTTP handler per container and prefer standard Node request and response objects
- You need a CLI that can listen on TCP, Unix sockets, or Windows named pipes with almost no framework setup
- You want opt-in body parsing with a default 1 MB limit and cached raw request bodies
- You are maintaining an existing micro 10 service whose composition model is already understood
- You are deploying to Vercel serverless functions: the README explicitly says micro is not intended for serverless and that Vercel already supplies equivalent request helpers
- You need routing, middleware, validation, authentication hooks, schemas, or an application plugin system: micro intentionally provides none of them
- You expect an active framework release cadence: version 10.0.1 was published in November 2022, and the only 2026 repository commit switched the workspace package manager
- You want one supported development command: the README says micro is production-only and points development users to the separate `micro-dev` package
- You need documentation that exactly matches the shipped TypeScript source: the README says development errors include stacks in responses, but 10.0.1's `sendError` always sends either the message or `Internal Server Error`, and the declared `createError` signature requires an original Error that README examples omit
Setup reality
Install `micro` and export a handler from the file named by `package.json` `main`, from `index.js`, or from an explicit CLI argument. Version 10.0.1 requires Node 16 or newer and includes TypeScript declarations, three pinned runtime dependencies, and a `micro` binary. The CLI defaults to `0.0.0.0:3000`; `-l tcp://host:port`, `-l unix:/path.sock`, and Windows `pipe:` endpoints are supported, and repeated `-l` flags open multiple listeners. Shell substitution in `micro -l tcp://0.0.0.0:$PORT` is a shell feature, so JSON scripts need platform-aware quoting and the README's fallback syntax only works in Bash. The project calls the CLI production-only and directs local work to the separate `micro-dev` tool, adding another package and workflow. Body parsing is deliberately opt-in. `json`, `text`, and `buffer` aggregate the request with a 1 MB default cap, cache the first raw body in a WeakMap, return 413 when the cap is exceeded, and return 400 for invalid input. They do not validate the resulting JSON; the TypeScript return type is `unknown`. A handler returning `null` produces 204, returning another value sends it, and returning `undefined` means you must eventually end the response yourself. Streams are piped as octet-stream, but the README makes stream error handling your responsibility. There is no router, middleware stack, request schema, access logging, CORS policy, graceful timeout policy, or TLS configuration. Compose those concerns around the handler or put them in a proxy. The README and published source also disagree in small but important places, so use the version 10 declarations and source as the final authority.
Patterns
Return a response from a CommonJS handlerexport-basic-handler
// index.js
module.exports = async (req, res) => {
return { ok: true, method: req.method };
};An object return value is serialized as JSON with status 200. The CLI uses `package.json` `main` first, then `index.js`.
Use an ESM default exportexport-esm-handler
// service.mjs
export default async function handler(req, res) {
return 'ready';
}
// start with: micro service.mjsThe version 10 CLI dynamically imports the entry file and reads its default export; a named export alone is rejected.
Parse and validate a bounded JSON bodyparse-json-body
const { createError, json } = require('micro');
module.exports = async (req) => {
const body = await json(req, { limit: '64kb' });
if (!body || typeof body !== 'object' || !('email' in body)) {
throw createError(422, 'email is required', new Error('invalid payload'));
}
return { accepted: body.email };
};`json()` returns `unknown` in the declarations and only parses syntax. Schema validation is your responsibility.
Reuse the cached request body in two formsread-text-and-buffer
const { buffer, text } = require('micro');
module.exports = async (req) => {
const raw = await buffer(req, { limit: 1024 * 1024 });
const value = await text(req);
return { bytes: raw.length, preview: value.slice(0, 40) };
};The first helper buffers the stream and caches it by request, so later body-helper calls do not try to consume the stream again.
Set an explicit status and response bodysend-status-response
const { send } = require('micro');
module.exports = async (req, res) => {
if (req.method !== 'POST') {
res.setHeader('Allow', 'POST');
send(res, 405, { error: 'method not allowed' });
return;
}
send(res, 202, { queued: true });
};After calling `send`, return `undefined`; returning another value would make micro attempt a second response.
Use the return-value contract deliberatelyreturn-no-content
module.exports = async (req, res) => {
if (req.method === 'DELETE') return null; // 204
if (req.method === 'HEAD') {
res.statusCode = 200;
res.end();
return undefined;
}
return 'ok';
};`null` means 204, a defined value is sent, and `undefined` tells micro that your code owns response completion.
Attach an HTTP status to a thrown errorthrow-http-error
const { createError } = require('micro');
async function requireUser(req) {
try {
return await authenticate(req);
} catch (cause) {
throw createError(401, 'unauthorized', cause);
}
}The shipped version 10 TypeScript signature requires the original Error as the third argument, even though older README examples omit it.
Wrap a handler with custom error outputcompose-error-handler
const { send } = require('micro');
const withErrors = (handler) => async (req, res) => {
try {
return await handler(req, res);
} catch (error) {
console.error(error);
send(res, error.statusCode || 500, { error: error.message });
}
};
module.exports = withErrors(async () => {
throw new Error('database unavailable');
});A wrapper is the middleware substitute. Do not expose stack traces or internal messages to clients in production.
Send a readable stream safelystream-response
const { createReadStream } = require('node:fs');
const { send } = require('micro');
module.exports = async (req, res) => {
const stream = createReadStream('/srv/files/report.pdf');
stream.on('error', (error) => {
console.error(error);
if (!res.headersSent) res.statusCode = 500;
res.destroy(error);
});
res.setHeader('Content-Type', 'application/pdf');
send(res, 200, stream);
};micro pipes streams but does not install your stream error policy; the README explicitly leaves that responsibility to the handler.
Mount a micro handler on a Node serverserve-programmatically
const http = require('node:http');
const { serve } = require('micro');
const server = http.createServer(serve(async (req) => ({
path: req.url,
uptime: process.uptime(),
})));
server.listen(3000);
for (const signal of ['SIGTERM', 'SIGINT']) {
process.once(signal, () => server.close(() => process.exit(0)));
}Programmatic use gives you control over server timeouts and shutdown. `serve()` itself only adapts the async handler to a Node request listener.
Select a CLI listen endpointlisten-on-endpoint
micro -l tcp://127.0.0.1:4000 service.js
micro -l unix:/tmp/orders.sock service.js
# Bash-only environment fallback:
micro -l tcp://0.0.0.0:${PORT-3000} service.jsThe `${PORT-3000}` fallback is Bash syntax. Use a cross-platform script or explicit environment handling when developers run other shells.
Alternatives
| Package | Registry | Pick it when |
|---|---|---|
| fastify | npm | You want routes, plugins, request schemas, validation, and structured logging in a performance-focused Node server |
| hono | npm | You want a small router that can target Node, edge runtimes, and serverless platforms |
| koa | npm | You like a minimal Node core but still need a mature async middleware composition model |
| express | npm | Compatibility, middleware availability, and familiar routing matter more than a single-function design |