Skip to content

Latest commit

 

History

History
176 lines (146 loc) · 5.99 KB

File metadata and controls

176 lines (146 loc) · 5.99 KB
title Send a JSON Response
id send-json-response
skillLevel beginner
applicationPatternId building-apis
summary Create and send a structured JSON response with the correct headers and status code.
tags
http
server
response
json
api
rule
description
Use Http.response.json to automatically serialize data structures into a JSON response.
author PaulJPhilp
related
handle-get-request
validate-request-body
lessonOrder 4

Guideline

To return a JavaScript object or value as a JSON response, use the Http.response.json(data) constructor.


Rationale

APIs predominantly communicate using JSON. The Http module provides a dedicated Http.response.json helper to make this as simple and robust as possible. Manually constructing a JSON response involves serializing the data and setting the correct HTTP headers, which is tedious and error-prone.

Using Http.response.json is superior because:

  1. Automatic Serialization: It safely handles the JSON.stringify operation for you, including handling potential circular references or other serialization errors.
  2. Correct Headers: It automatically sets the Content-Type: application/json; charset=utf-8 header. This is critical for clients to correctly interpret the response body. Forgetting this header is a common source of bugs in manually constructed APIs.
  3. Simplicity and Readability: Your intent is made clear with a single, declarative function call. The code is cleaner and focuses on the data being sent, not the mechanics of HTTP.
  4. Composability: It creates a standard Http.response object that works seamlessly with all other parts of the Effect Http module.

Good Example

This example defines a route that fetches a user object and returns it as a JSON response. The Http.response.json function handles all the necessary serialization and header configuration.

import { Effect, Context, Duration, Layer } from "effect";
import { NodeContext, NodeHttpServer } from "@effect/platform-node";
import { createServer } from "node:http";

const PORT = 3459; // Changed port to avoid conflicts

// Define HTTP Server service
class JsonServer extends Effect.Service<JsonServer>()("JsonServer", {
  sync: () => ({
    handleRequest: () =>
      Effect.succeed({
        status: 200,
        headers: { "Content-Type": "application/json" },
        body: JSON.stringify({
          message: "Hello, JSON!",
          timestamp: new Date().toISOString(),
        }),
      }),
  }),
}) {}

// Create and run the server
const program = Effect.gen(function* () {
  const jsonServer = yield* JsonServer;

  // Create and start HTTP server
  const server = createServer((req, res) => {
    const requestHandler = Effect.gen(function* () {
      try {
        const response = yield* jsonServer.handleRequest();
        res.writeHead(response.status, response.headers);
        res.end(response.body);
        // Log the response for demonstration
        yield* Effect.logInfo(`Sent JSON response: ${response.body}`);
      } catch (error: any) {
        res.writeHead(500, { "Content-Type": "application/json" });
        res.end(JSON.stringify({ error: "Internal Server Error" }));
        yield* Effect.logError(`Request error: ${error.message}`);
      }
    });

    Effect.runPromise(requestHandler);
  });

  // Start server with error handling
  yield* Effect.async<void, Error>((resume) => {
    server.on("error", (error: NodeJS.ErrnoException) => {
      if (error.code === "EADDRINUSE") {
        resume(Effect.fail(new Error(`Port ${PORT} is already in use`)));
      } else {
        resume(Effect.fail(error));
      }
    });

    server.listen(PORT, () => {
      resume(Effect.succeed(void 0));
    });
  });

  yield* Effect.logInfo(`Server running at http://localhost:${PORT}`);
  yield* Effect.logInfo("Try: curl http://localhost:3459");

  // Run for a short time to demonstrate
  yield* Effect.sleep(Duration.seconds(3));

  // Shutdown gracefully
  yield* Effect.sync(() => server.close());
  yield* Effect.logInfo("Server shutdown complete");
}).pipe(
  Effect.catchAll((error) =>
    Effect.gen(function* () {
      yield* Effect.logError(`Server error: ${error.message}`);
      return error;
    })
  ),
  // Merge layers and provide them in a single call to ensure proper lifecycle management
  Effect.provide(Layer.merge(JsonServer.Default, NodeContext.layer))
);

// Run the program
// Use Effect.runFork for server applications that shouldn't resolve the promise
Effect.runPromise(
  program.pipe(
    // Ensure the Effect has no remaining context requirements for runPromise
    Effect.map(() => undefined)
  )
);

Anti-Pattern

The anti-pattern is to manually serialize the data to a string and set the headers yourself. This is verbose and introduces opportunities for error.

import { Effect } from "effect";
import { Http, NodeHttpServer, NodeRuntime } from "@effect/platform-node";

const getUserRoute = Http.router.get(
  "/users/1",
  Effect.succeed({ id: 1, name: "Paul", team: "Effect" }).pipe(
    Effect.flatMap((user) => {
      // Manually serialize the object to a JSON string.
      const jsonString = JSON.stringify(user);
      // Create a text response with the string.
      const response = Http.response.text(jsonString);
      // Manually set the Content-Type header.
      return Effect.succeed(
        Http.response.setHeader(
          response,
          "Content-Type",
          "application/json; charset=utf-8"
        )
      );
    })
  )
);

const app = Http.router.empty.pipe(Http.router.addRoute(getUserRoute));

const program = Http.server
  .serve(app)
  .pipe(Effect.provide(NodeHttpServer.layer({ port: 3000 })));

NodeRuntime.runMain(program);

This manual approach is unnecessarily complex. It forces you to remember to perform both the serialization and the header configuration. If you forget the setHeader call, many clients will fail to parse the response correctly. The Http.response.json helper eliminates this entire class of potential bugs.