Framework Integrations

Use phoneng with Hono, Express, and Next.js for validating phone numbers in API routes and server actions.

phoneng works with any JavaScript framework. These examples show common patterns for API routes and server actions.

Hono

Route Handler

import { Hono } from "hono";
import { zValidator } from "@hono/zod-validator";
import { z } from "zod";
import { parse } from "phoneng";

const app = new Hono();

const schema = z.object({
  phone: z.string().transform((val, ctx) => {
    const result = parse(val);
    if (!result.valid) {
      ctx.addIssue({
        code: z.ZodIssueCode.custom,
        message: result.reason,
      });
      return z.NEVER;
    }
    return result;
  }),
});

app.post("/api/verify-phone", zValidator("json", schema), async (c) => {
  const { phone } = c.req.valid("json");

  return c.json({
    success: true,
    data: {
      e164: phone.e164,
      network: phone.network,
      type: phone.type,
    },
  });
});

export default app;

Middleware

Create reusable middleware for phone validation:

import { createMiddleware } from "hono/factory";
import { parse, type ParseSuccess } from "phoneng";

type PhoneEnv = {
  Variables: {
    phone: ParseSuccess;
  };
};

export const validatePhone = (field: string) =>
  createMiddleware<PhoneEnv>(async (c, next) => {
    const body = await c.req.json();
    const result = parse(body[field]);

    if (!result.valid) {
      return c.json({ error: `Invalid phone: ${result.reason}` }, 400);
    }

    c.set("phone", result);
    await next();
  });

app.post("/api/send-otp", validatePhone("phone"), async (c) => {
  const phone = c.get("phone");
  await sendOTP(phone.e164);
  return c.json({ success: true });
});

Express

Route Handler

import express from "express";
import { parse } from "phoneng";

const app = express();
app.use(express.json());

app.post("/api/verify-phone", (req, res) => {
  const { phone } = req.body;

  if (!phone || typeof phone !== "string") {
    return res.status(400).json({ error: "Phone number is required" });
  }

  const result = parse(phone);

  if (!result.valid) {
    return res.status(400).json({
      error: "Invalid phone number",
      code: result.reason,
    });
  }

  res.json({
    success: true,
    data: {
      e164: result.e164,
      national: result.national,
      network: result.network,
      type: result.type,
    },
  });
});

Middleware

import { Request, Response, NextFunction } from "express";
import { parse, type ParseSuccess } from "phoneng";

declare global {
  namespace Express {
    interface Request {
      parsedPhone?: ParseSuccess;
    }
  }
}

export function validatePhone(field: string) {
  return (req: Request, res: Response, next: NextFunction) => {
    const value = req.body[field];

    if (!value) {
      return res.status(400).json({ error: `${field} is required` });
    }

    const result = parse(value);

    if (!result.valid) {
      return res.status(400).json({
        error: `Invalid ${field}`,
        code: result.reason,
      });
    }

    req.parsedPhone = result;
    next();
  };
}

app.post("/api/register", validatePhone("phone"), (req, res) => {
  const phone = req.parsedPhone;
  res.json({ e164: phone.e164 });
});

Next.js App Router

Route Handler

import { NextRequest, NextResponse } from "next/server";
import { parse } from "phoneng";

export async function POST(request: NextRequest) {
  const body = await request.json();
  const { phone } = body;

  if (!phone || typeof phone !== "string") {
    return NextResponse.json(
      { error: "Phone number is required" },
      { status: 400 },
    );
  }

  const result = parse(phone);

  if (!result.valid) {
    return NextResponse.json(
      { error: "Invalid phone number", code: result.reason },
      { status: 400 },
    );
  }

  return NextResponse.json({
    success: true,
    data: {
      e164: result.e164,
      network: result.network,
    },
  });
}

Server Action

"use server";

import { parse, type ParseResult } from "phoneng";

type ActionResult =
  | { success: true; e164: string; network: string }
  | { success: false; error: string };

export async function verifyPhone(formData: FormData): Promise<ActionResult> {
  const phone = formData.get("phone");

  if (!phone || typeof phone !== "string") {
    return { success: false, error: "Phone number is required" };
  }

  const result = parse(phone);

  if (!result.valid) {
    return { success: false, error: result.reason };
  }

  return {
    success: true,
    e164: result.e164,
    network: result.network,
  };
}

Client Component with Server Action

"use client";

import { useActionState } from "react";
import { verifyPhone } from "./actions";

export function PhoneForm() {
  const [state, action, pending] = useActionState(verifyPhone, null);

  return (
    <form action={action}>
      <input type="tel" name="phone" placeholder="08031234567" required />
      <button type="submit" disabled={pending}>
        {pending ? "Verifying..." : "Verify"}
      </button>

      {state && !state.success && <p className="error">{state.error}</p>}

      {state && state.success && (
        <p className="success">
          Valid {state.network} number: {state.e164}
        </p>
      )}
    </form>
  );
}

Fastify

import Fastify from "fastify";
import { parse } from "phoneng";

const fastify = Fastify();

fastify.post("/api/verify-phone", async (request, reply) => {
  const { phone } = request.body as { phone?: string };

  if (!phone) {
    return reply.status(400).send({ error: "Phone required" });
  }

  const result = parse(phone);

  if (!result.valid) {
    return reply.status(400).send({
      error: "Invalid phone number",
      code: result.reason,
    });
  }

  return {
    success: true,
    data: {
      e164: result.e164,
      network: result.network,
    },
  };
});

tRPC

import { initTRPC, TRPCError } from "@trpc/server";
import { z } from "zod";
import { parse } from "phoneng";

const t = initTRPC.create();

const nigerianPhone = z.string().transform((val, ctx) => {
  const result = parse(val);
  if (!result.valid) {
    ctx.addIssue({
      code: z.ZodIssueCode.custom,
      message: result.reason,
    });
    return z.NEVER;
  }
  return result;
});

export const appRouter = t.router({
  verifyPhone: t.procedure
    .input(z.object({ phone: nigerianPhone }))
    .mutation(async ({ input }) => {
      return {
        e164: input.phone.e164,
        network: input.phone.network,
      };
    }),
});
MIT License GitHub · npm

Network data from NCC allocations. MNP may affect actual carriers.